Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12de75f882 | ||
|
|
e4273f5150 | ||
|
|
32635839c6 | ||
|
|
92f39dde1a | ||
|
|
db21b8f680 | ||
|
|
738d5cbb35 | ||
|
|
309578130c | ||
|
|
b452cf1ab4 | ||
|
|
15f9d1bde6 | ||
|
|
573b766fc4 | ||
|
|
5437bef9c5 | ||
|
|
19e3047282 | ||
|
|
27428cdb0e | ||
|
|
baa4879e2c | ||
|
|
cae402b231 | ||
|
|
ec882c5aa8 | ||
|
|
4941849503 | ||
|
|
79bdcb8c2c | ||
|
|
c8ede8dd4f | ||
|
|
94a7a55471 | ||
|
|
e1a8d3ffa8 | ||
|
|
ce28d4bc31 | ||
|
|
4fbdf7902a | ||
|
|
20d11406a8 | ||
|
|
2fc4ae9948 | ||
|
|
77ff67d2be | ||
|
|
eb9ae2b1e4 | ||
|
|
3893e9f890 | ||
|
|
faa747a5f1 | ||
|
|
3f49ee2c13 | ||
|
|
0c9211e775 | ||
|
|
beab876d7b | ||
|
|
b71de54720 | ||
|
|
0b8f5d345c | ||
|
|
f6683f1533 | ||
|
|
e8bf8f1ce3 | ||
|
|
d147f262b5 | ||
|
|
55bc04198f | ||
|
|
be220533c2 | ||
|
|
a528743c83 | ||
|
|
d931784ba0 | ||
|
|
3488dbab71 | ||
|
|
cc4af5e812 | ||
|
|
865ed909fa | ||
|
|
563686202f | ||
|
|
cfa61b363e | ||
|
|
04a2c59392 | ||
|
|
322185f241 | ||
|
|
3d3c128864 | ||
|
|
f07e8400cf | ||
|
|
fa5904c952 | ||
|
|
07ad0d21b8 | ||
|
|
6163dc6fcc |
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 lockbitchat
|
||||
Copyright (c) 2025 SecureBitChat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
16
README.md
@@ -1,9 +1,9 @@
|
||||
# lockbit-chat
|
||||
# securebit-chat
|
||||
🔒 World's most secure P2P messenger with Lightning Network integration. End-to-end encryption, pay-per-session model, zero data collection. WebRTC direct connections, quantum-resistant roadmap. Privacy-first communication for the Bitcoin age ⚡
|
||||
|
||||
🛡️ LockBit.chat - Enhanced Security Edition
|
||||
🛡️ SecureBit.chat - Enhanced Security Edition
|
||||
🎯 About the Project
|
||||
LockBit.chat is a revolutionary P2P messenger that combines:
|
||||
SecureBit.chat is a revolutionary P2P messenger that combines:
|
||||
|
||||
Military-grade cryptography (ECDH P-384 + AES-GCM 256)
|
||||
Lightning Network payments for sessions
|
||||
@@ -35,7 +35,7 @@ No metadata collection
|
||||
|
||||
🚀 Quick Start
|
||||
|
||||
Open: https://lockbit.chat
|
||||
Open: https://SecureBit.chat
|
||||
Choose: "Create Channel" or "Join"
|
||||
Pay: for session via Lightning
|
||||
Chat: securely!
|
||||
@@ -71,8 +71,8 @@ P2P: WebRTC DataChannels
|
||||
Payments: Lightning Network / WebLN
|
||||
|
||||
Local Development:
|
||||
bashgit clone https://github.com/lockbitchat/lockbit-chat.git
|
||||
cd lockbit-chat
|
||||
bashgit clone https://github.com/SecureBitChat/securebit-chat.git
|
||||
cd securebit-chat
|
||||
python -m http.server 8000
|
||||
# Open http://localhost:8000
|
||||
🤝 Contributing
|
||||
@@ -88,8 +88,8 @@ How to help:
|
||||
📄 License
|
||||
MIT License with mandatory attribution
|
||||
⚠️ Disclaimer
|
||||
LockBit.chat is provided "as is". Use at your own risk. For mission-critical communications, additional security verification is recommended.
|
||||
SecureBit.chat is provided "as is". Use at your own risk. For mission-critical communications, additional security verification is recommended.
|
||||
📞 Contacts
|
||||
|
||||
🌐 Website: https://lockbit.chat
|
||||
🌐 Website: https://SecureBit.chat
|
||||
📧 Email: lockbitchat@tutanota.com
|
||||
|
||||
186
SECURITY.md
@@ -2,31 +2,47 @@
|
||||
|
||||
## 🛡️ Security Overview
|
||||
|
||||
LockBit.chat is built with security-first principles. We take security vulnerabilities seriously and appreciate responsible disclosure from the security community.
|
||||
SecureBit.chat is built with security-first principles and implements **military-grade security** with 12-layer protection system. We take security vulnerabilities seriously and appreciate responsible disclosure from the security community.
|
||||
|
||||
## 🔒 Security Features
|
||||
**Current Security Status:** 🔒 **MAXIMUM SECURITY (Stage 4)** - Exceeds government-grade communication standards
|
||||
|
||||
### Cryptographic Implementation
|
||||
- **Key Exchange:** ECDH P-384 (NIST recommended curve)
|
||||
- **Encryption:** AES-GCM 256-bit with authenticated encryption
|
||||
- **Digital Signatures:** ECDSA P-384 for message authenticity
|
||||
- **Perfect Forward Secrecy:** Automatic key rotation every 5 minutes
|
||||
- **Non-extractable Keys:** All cryptographic keys are hardware-protected
|
||||
- **MITM Protection:** Out-of-band verification codes
|
||||
## 🔒 Enhanced Security Features (Stage 4)
|
||||
|
||||
### Architecture Security
|
||||
### Multi-Layer Cryptographic Implementation
|
||||
- **Key Exchange:** ECDH P-384 (NIST recommended curve) with non-extractable keys
|
||||
- **Primary Encryption:** AES-GCM 256-bit with authenticated encryption
|
||||
- **Nested Encryption:** Additional AES-GCM 256-bit layer for maximum protection
|
||||
- **Metadata Protection:** Separate AES-GCM 256-bit encryption for message metadata
|
||||
- **Digital Signatures:** ECDSA P-384 with SHA-384 for message authenticity and MITM protection
|
||||
- **Perfect Forward Secrecy:** Automatic key rotation every 5 minutes with secure key versioning
|
||||
- **Non-extractable Keys:** All cryptographic keys are hardware-protected and non-exportable
|
||||
- **Enhanced Replay Protection:** Multi-factor protection with sequence numbers, message IDs, and timestamps
|
||||
|
||||
### Advanced Traffic Obfuscation
|
||||
- **Packet Padding:** Random padding (64-512 bytes) to hide real message sizes
|
||||
- **Anti-Fingerprinting:** Advanced traffic pattern obfuscation and timing randomization
|
||||
- **Fake Traffic Generation:** Invisible decoy messages for traffic analysis protection
|
||||
- **Message Chunking:** Split messages into random-sized chunks with variable delays
|
||||
- **Packet Reordering Protection:** Sequence-based packet reassembly with timeout handling
|
||||
- **Decoy Channels:** Multiple fake communication channels to confuse attackers
|
||||
|
||||
### Enhanced Security Architecture
|
||||
- **Zero-trust Model:** No central servers to compromise
|
||||
- **P2P Direct:** WebRTC encrypted channels
|
||||
- **No Data Persistence:** Messages exist only in memory
|
||||
- **Rate Limiting:** Protection against spam and DoS
|
||||
- **Replay Protection:** Sequence numbers and message IDs
|
||||
- **P2P Direct:** WebRTC encrypted channels with enhanced validation
|
||||
- **No Data Persistence:** Messages exist only in memory, automatic cleanup
|
||||
- **Enhanced Rate Limiting:** 60 messages/minute, 5 connections/5 minutes with cryptographic verification
|
||||
- **Session Security:** 64-byte salts, unique session IDs, and replay attack prevention
|
||||
- **MITM Protection:** Out-of-band verification codes with enhanced validation
|
||||
|
||||
## 🚨 Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 4.0.x | ✅ Yes |
|
||||
| < 4.0 | ❌ No |
|
||||
| Version | Security Level | Supported |
|
||||
| ------- | -------------- | ------------------ |
|
||||
| 4.0.x | MAXIMUM | ✅ Yes (12 layers)|
|
||||
| 3.x.x | HIGH | ⚠️ Limited |
|
||||
| < 3.0 | BASIC | ❌ No |
|
||||
|
||||
**Recommendation:** Upgrade to 4.0.x immediately for maximum security protection.
|
||||
|
||||
## 📋 Reporting a Vulnerability
|
||||
|
||||
@@ -36,9 +52,9 @@ For **critical security issues** that could compromise user safety:
|
||||
**DO NOT** create a public GitHub issue.
|
||||
|
||||
**Contact us privately:**
|
||||
- 📧 **Email:** security@lockbit.chat (PGP key below)
|
||||
- 📧 **Email:** security@SecureBit.chat (PGP key below)
|
||||
- 🔒 **Signal:** +[REDACTED] (ask for Signal number via email)
|
||||
- 🔐 **Keybase:** @lockbitchat
|
||||
- 🔐 **Keybase:** @SecureBitChat
|
||||
|
||||
### 🟡 Non-Critical Issues
|
||||
For general security improvements or non-critical findings:
|
||||
@@ -47,7 +63,7 @@ For general security improvements or non-critical findings:
|
||||
|
||||
## 📝 Vulnerability Disclosure Process
|
||||
|
||||
1. **Report:** Send details to security@lockbit.chat
|
||||
1. **Report:** Send details to security@SecureBit.chat
|
||||
2. **Acknowledgment:** We'll respond within 24 hours
|
||||
3. **Investigation:** We'll investigate and keep you updated
|
||||
4. **Fix:** We'll develop and test a fix
|
||||
@@ -61,10 +77,10 @@ For general security improvements or non-critical findings:
|
||||
|
||||
## 🏆 Security Hall of Fame
|
||||
|
||||
We maintain a hall of fame for security researchers who help improve LockBit.chat:
|
||||
We maintain a hall of fame for security researchers who help improve SecureBit.chat:
|
||||
|
||||
<!-- Security researchers will be listed here -->
|
||||
*Be the first to help secure LockBit.chat!*
|
||||
*Be the first to help secure SecureBit.chat!*
|
||||
|
||||
## 🔍 Security Audit History
|
||||
|
||||
@@ -74,33 +90,71 @@ We maintain a hall of fame for security researchers who help improve LockBit.cha
|
||||
|
||||
### Internal Security Measures
|
||||
- **Code Review:** All cryptographic code reviewed by multiple developers
|
||||
- **Testing:** Comprehensive security test suite
|
||||
- **Security Testing:** Comprehensive 12-layer security test suite
|
||||
- **Dependencies:** Regular security updates for all dependencies
|
||||
- **Vulnerability Testing:** Automated testing for all 12 security layers
|
||||
|
||||
## 📊 Security Architecture (Stage 4)
|
||||
|
||||
```
|
||||
12-Layer Security Architecture:
|
||||
├── Layer 1: Enhanced Authentication (ECDSA P-384 + SHA-384)
|
||||
├── Layer 2: Key Exchange (ECDH P-384, non-extractable keys)
|
||||
├── Layer 3: Metadata Protection (AES-256-GCM + 64-byte salt)
|
||||
├── Layer 4: Message Encryption (Enhanced with sequence numbers)
|
||||
├── Layer 5: Nested Encryption (Additional AES-256-GCM layer)
|
||||
├── Layer 6: Packet Padding (64-512 bytes random obfuscation)
|
||||
├── Layer 7: Anti-Fingerprinting (Advanced pattern obfuscation)
|
||||
├── Layer 8: Packet Reordering Protection (Sequence + timeout)
|
||||
├── Layer 9: Message Chunking (Random delays + sizes)
|
||||
├── Layer 10: Fake Traffic Generation (Invisible decoy messages)
|
||||
├── Layer 11: Enhanced Rate Limiting (Cryptographic verification)
|
||||
└── Layer 12: Perfect Forward Secrecy (5-minute key rotation)
|
||||
```
|
||||
|
||||
### Security Metrics
|
||||
- **Encryption Strength:** Triple-layer AES-256-GCM
|
||||
- **Key Security:** P-384 ECDH/ECDSA (equivalent to 7680-bit RSA)
|
||||
- **Forward Secrecy:** Complete (automatic key rotation)
|
||||
- **Traffic Analysis Protection:** Maximum (6-layer obfuscation)
|
||||
- **Attack Surface:** Minimal (P2P, no central servers)
|
||||
|
||||
## 🛠️ Security Best Practices for Users
|
||||
|
||||
### For Maximum Security:
|
||||
1. **Verify Authenticity:** Always verify out-of-band codes
|
||||
2. **Use Official Source:** Only use https://lockbit.chat
|
||||
3. **Keep Updated:** Use the latest version
|
||||
1. **Verify Authenticity:** Always verify out-of-band codes (enhanced 6-digit format)
|
||||
2. **Use Official Source:** Only use https://SecureBit.chat
|
||||
3. **Keep Updated:** Use version 4.0.x for maximum security
|
||||
4. **Secure Environment:** Use updated browsers on secure devices
|
||||
5. **Lightning Wallets:** Use reputable Lightning wallets (Alby, Zeus, etc.)
|
||||
6. **Monitor Security Status:** Check for "MAXIMUM SECURITY" indicator in chat
|
||||
|
||||
### Security Indicators:
|
||||
- ✅ **Green Shield:** MAXIMUM SECURITY (Stage 4) active
|
||||
- 🟡 **Yellow Shield:** HIGH SECURITY (Stage 3)
|
||||
- 🔴 **Red Shield:** Security issues detected
|
||||
|
||||
### Red Flags:
|
||||
- ❌ Codes don't match during verification
|
||||
- ❌ Verification codes don't match
|
||||
- ❌ Security level below Stage 4
|
||||
- ❌ Unusual connection behavior
|
||||
- ❌ Requests for private keys or seed phrases
|
||||
- ❌ Unofficial domains or mirrors
|
||||
- ❌ Missing security layer notifications
|
||||
|
||||
## 🔬 Security Research Guidelines
|
||||
|
||||
### Scope
|
||||
**In Scope:**
|
||||
- ✅ Cryptographic implementation flaws
|
||||
- ✅ Cryptographic implementation flaws in any of the 12 layers
|
||||
- ✅ WebRTC security issues
|
||||
- ✅ Authentication bypass
|
||||
- ✅ Authentication bypass attempts
|
||||
- ✅ Input validation vulnerabilities
|
||||
- ✅ Client-side security issues
|
||||
- ✅ Traffic analysis vulnerabilities
|
||||
- ✅ Perfect Forward Secrecy implementation
|
||||
- ✅ Anti-fingerprinting bypass techniques
|
||||
- ✅ Fake traffic detection methods
|
||||
|
||||
**Out of Scope:**
|
||||
- ❌ Social engineering attacks
|
||||
@@ -108,59 +162,73 @@ We maintain a hall of fame for security researchers who help improve LockBit.cha
|
||||
- ❌ DoS attacks on user connections
|
||||
- ❌ Issues requiring physical access
|
||||
- ❌ Lightning Network protocol issues
|
||||
- ❌ Browser security vulnerabilities
|
||||
|
||||
### Research Ethics
|
||||
- **No Disruption:** Don't interfere with live users
|
||||
- **Responsible Disclosure:** Follow our disclosure timeline
|
||||
- **No Data Harvesting:** Don't collect user communications
|
||||
- **Legal Compliance:** Follow all applicable laws
|
||||
- **Respect Privacy:** Don't attempt to break active encrypted sessions
|
||||
|
||||
## 📊 Security Metrics
|
||||
## 🔄 Recent Security Updates (Version 4.0)
|
||||
|
||||
We track and publish these security metrics:
|
||||
- **Response Time:** Average time to acknowledge reports
|
||||
- **Fix Time:** Average time to deploy fixes
|
||||
- **Vulnerability Count:** Number of reported/fixed issues
|
||||
- **Audit Coverage:** Percentage of code under security review
|
||||
### Major Security Enhancements:
|
||||
- ✅ **Implemented 12-layer security architecture**
|
||||
- ✅ **Added Perfect Forward Secrecy with automatic key rotation**
|
||||
- ✅ **Enhanced MITM protection with ECDSA signatures**
|
||||
- ✅ **Implemented traffic obfuscation (fake traffic, padding, chunking)**
|
||||
- ✅ **Added anti-fingerprinting protection**
|
||||
- ✅ **Fixed demo session creation vulnerability**
|
||||
- ✅ **Eliminated session replay attacks**
|
||||
- ✅ **Enhanced rate limiting with cryptographic verification**
|
||||
|
||||
## 🔄 Security Updates
|
||||
|
||||
### How We Notify Users:
|
||||
- **Critical:** Immediate notification on website
|
||||
- **Important:** GitHub releases and social media
|
||||
- **Minor:** Regular update cycles
|
||||
|
||||
### Auto-Update Policy:
|
||||
- **Critical Security Fixes:** Automatic for web version
|
||||
- **Feature Updates:** User-controlled
|
||||
- **Breaking Changes:** Advance notice with migration guide
|
||||
|
||||
## 🤝 Working with Security Researchers
|
||||
|
||||
We value the security community and offer:
|
||||
- **Recognition:** Public credit and hall of fame listing
|
||||
- **Swag:** LockBit.chat merchandise for quality reports
|
||||
- **References:** LinkedIn recommendations for exceptional work
|
||||
- **Early Access:** Beta access to new security features
|
||||
### Bug Fixes:
|
||||
- 🔧 **Fixed fake traffic visibility in user interface**
|
||||
- 🔧 **Resolved message processing conflicts**
|
||||
- 🔧 **Improved security layer error handling**
|
||||
- 🔧 **Enhanced session validation**
|
||||
|
||||
## 📚 Security Resources
|
||||
|
||||
### Technical Documentation:
|
||||
- [Cryptographic Architecture](docs/CRYPTOGRAPHY.md)
|
||||
- [12-Layer Security Architecture](docs/SECURITY-ARCHITECTURE.md)
|
||||
- [Cryptographic Implementation](docs/CRYPTOGRAPHY.md)
|
||||
- [P2P Security Model](docs/P2P-SECURITY.md)
|
||||
- [Lightning Integration Security](docs/LIGHTNING-SECURITY.md)
|
||||
- [Traffic Obfuscation Guide](docs/TRAFFIC-OBFUSCATION.md)
|
||||
|
||||
### External Resources:
|
||||
- [WebRTC Security Guide](https://webrtc-security.github.io/)
|
||||
- [Web Crypto API Best Practices](https://www.w3.org/TR/WebCryptoAPI/)
|
||||
- [Lightning Network Security](https://lightning.network/lightning-network-paper.pdf)
|
||||
- [NIST Cryptographic Standards](https://csrc.nist.gov/)
|
||||
|
||||
### Security Verification:
|
||||
```bash
|
||||
# Verify current security status in browser console:
|
||||
webrtcManager.getSecurityStatus()
|
||||
# Expected: { stage: 4, securityLevel: 'MAXIMUM', activeFeatures: 12 }
|
||||
```
|
||||
|
||||
## 📞 Contact Information
|
||||
|
||||
- **Security Team:** security@lockbit.chat
|
||||
- **Security Team:** security@SecureBit.chat
|
||||
- **General Contact:** lockbitchat@tutanota.com
|
||||
- **GitHub Issues:** https://github.com/lockbitchat/lockbit-chat/issues
|
||||
- **GitHub Issues:** https://github.com/SecureBitChat/securebit-chat/issues
|
||||
|
||||
## 🏅 Security Achievements
|
||||
|
||||
SecureBit.chat v4.0 provides:
|
||||
- **🥇 Military-Grade Security:** 12-layer protection system
|
||||
- **🥇 Government-Level Encryption:** Triple AES-256-GCM + P-384 ECDH/ECDSA
|
||||
- **🥇 Perfect Forward Secrecy:** Complete with automatic key rotation
|
||||
- **🥇 Traffic Analysis Protection:** Maximum with 6-layer obfuscation
|
||||
- **🥇 Zero-Trust Architecture:** No central points of failure
|
||||
|
||||
**Security Rating: MAXIMUM** - Exceeds most government and military communication standards.
|
||||
|
||||
---
|
||||
|
||||
*This security policy is reviewed and updated quarterly. Last updated: 08/09/2025*
|
||||
*This security policy is reviewed and updated quarterly. Last updated: January 14, 2025*
|
||||
*Security implementation verified and tested as of Version 4.0*
|
||||
859
doc/API.md
Normal file
@@ -0,0 +1,859 @@
|
||||
# SecureBit.chat API Documentation
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
|
||||
SecureBit.chat is built as a client-side application with no backend servers. The "API" consists of JavaScript classes and methods that handle cryptography, P2P connections, and Lightning Network integration.
|
||||
|
||||
## 📚 Core Classes
|
||||
|
||||
### 🔐 EnhancedSecureCryptoUtils
|
||||
|
||||
Central cryptographic utilities class providing military-grade encryption.
|
||||
|
||||
#### Key Generation
|
||||
|
||||
##### `generateECDHKeyPair()`
|
||||
``javascript
|
||||
static async generateECDHKeyPair(): Promise<CryptoKeyPair>
|
||||
Generates non-extractable ECDH P-384 key pair for secure key exchange.
|
||||
Returns: CryptoKeyPair with P-384 keys
|
||||
Throws: Error if key generation fails
|
||||
Example:
|
||||
javascriptconst keyPair = await EnhancedSecureCryptoUtils.generateECDHKeyPair();
|
||||
console.log(keyPair.privateKey.algorithm.namedCurve); // "P-384"
|
||||
generateECDSAKeyPair()
|
||||
javascriptstatic async generateECDSAKeyPair(): Promise<CryptoKeyPair>
|
||||
Generates non-extractable ECDSA P-384 key pair for digital signatures.
|
||||
Returns: CryptoKeyPair for signing and verification
|
||||
Throws: Error if key generation fails
|
||||
Encryption/Decryption
|
||||
encryptMessage()
|
||||
javascriptstatic async encryptMessage(
|
||||
message: string,
|
||||
encryptionKey: CryptoKey,
|
||||
macKey: CryptoKey,
|
||||
metadataKey: CryptoKey,
|
||||
messageId: string,
|
||||
sequenceNumber: number = 0
|
||||
): Promise<EncryptedMessage>
|
||||
Encrypts a message with metadata protection and sequence numbers.
|
||||
Parameters:
|
||||
|
||||
message - Plaintext message (max 2000 chars)
|
||||
encryptionKey - AES-GCM 256-bit key
|
||||
macKey - HMAC key for authentication
|
||||
metadataKey - Key for metadata encryption
|
||||
messageId - Unique message identifier
|
||||
sequenceNumber - Message sequence for replay protection
|
||||
|
||||
Returns:
|
||||
typescriptinterface EncryptedMessage {
|
||||
messageIv: number[];
|
||||
messageData: number[];
|
||||
metadataIv: number[];
|
||||
metadataData: number[];
|
||||
mac: number[];
|
||||
version: string;
|
||||
}
|
||||
Example:
|
||||
javascriptconst encrypted = await EnhancedSecureCryptoUtils.encryptMessage(
|
||||
"Hello, secure world!",
|
||||
encryptionKey,
|
||||
macKey,
|
||||
metadataKey,
|
||||
"msg_12345",
|
||||
42
|
||||
);
|
||||
decryptMessage()
|
||||
javascriptstatic async decryptMessage(
|
||||
encryptedPayload: EncryptedMessage,
|
||||
encryptionKey: CryptoKey,
|
||||
macKey: CryptoKey,
|
||||
metadataKey: CryptoKey,
|
||||
expectedSequenceNumber?: number
|
||||
): Promise<DecryptedMessage>
|
||||
Decrypts and verifies an encrypted message.
|
||||
Returns:
|
||||
typescriptinterface DecryptedMessage {
|
||||
message: string;
|
||||
messageId: string;
|
||||
timestamp: number;
|
||||
sequenceNumber: number;
|
||||
}
|
||||
Key Exchange
|
||||
deriveSharedKeys()
|
||||
javascriptstatic async deriveSharedKeys(
|
||||
privateKey: CryptoKey,
|
||||
publicKey: CryptoKey,
|
||||
salt: Uint8Array
|
||||
): Promise<SharedKeys>
|
||||
Derives shared encryption keys using ECDH + HKDF.
|
||||
Parameters:
|
||||
|
||||
privateKey - Local ECDH private key
|
||||
publicKey - Remote ECDH public key
|
||||
salt - 64-byte cryptographic salt
|
||||
|
||||
Returns:
|
||||
typescriptinterface SharedKeys {
|
||||
encryptionKey: CryptoKey;
|
||||
macKey: CryptoKey;
|
||||
metadataKey: CryptoKey;
|
||||
fingerprint: string;
|
||||
timestamp: number;
|
||||
version: string;
|
||||
}
|
||||
Example:
|
||||
javascriptconst salt = EnhancedSecureCryptoUtils.generateSalt();
|
||||
const sharedKeys = await EnhancedSecureCryptoUtils.deriveSharedKeys(
|
||||
localPrivateKey,
|
||||
remotePublicKey,
|
||||
salt
|
||||
);
|
||||
console.log('Key fingerprint:', sharedKeys.fingerprint);
|
||||
Digital Signatures
|
||||
signData()
|
||||
javascriptstatic async signData(
|
||||
privateKey: CryptoKey,
|
||||
data: string | Uint8Array
|
||||
): Promise<number[]>
|
||||
Signs data with ECDSA P-384.
|
||||
Parameters:
|
||||
|
||||
privateKey - ECDSA private key
|
||||
data - Data to sign
|
||||
|
||||
Returns: Signature as byte array
|
||||
Example:
|
||||
javascriptconst signature = await EnhancedSecureCryptoUtils.signData(
|
||||
ecdsaPrivateKey,
|
||||
"Important message"
|
||||
);
|
||||
verifySignature()
|
||||
javascriptstatic async verifySignature(
|
||||
publicKey: CryptoKey,
|
||||
signature: number[],
|
||||
data: string | Uint8Array
|
||||
): Promise<boolean>
|
||||
Verifies ECDSA signature.
|
||||
Returns: true if signature is valid
|
||||
Authentication
|
||||
generateMutualAuthChallenge()
|
||||
javascriptstatic generateMutualAuthChallenge(): AuthChallenge
|
||||
Generates cryptographic challenge for mutual authentication.
|
||||
Returns:
|
||||
typescriptinterface AuthChallenge {
|
||||
challenge: number[];
|
||||
timestamp: number;
|
||||
nonce: number[];
|
||||
version: string;
|
||||
}
|
||||
createAuthProof()
|
||||
javascriptstatic async createAuthProof(
|
||||
challenge: AuthChallenge,
|
||||
privateKey: CryptoKey,
|
||||
publicKey: CryptoKey
|
||||
): Promise<AuthProof>
|
||||
Creates cryptographic proof for challenge response.
|
||||
Returns:
|
||||
typescriptinterface AuthProof {
|
||||
challenge: number[];
|
||||
timestamp: number;
|
||||
nonce: number[];
|
||||
responseTimestamp: number;
|
||||
publicKeyHash: string;
|
||||
signature: number[];
|
||||
version: string;
|
||||
}
|
||||
verifyAuthProof()
|
||||
javascriptstatic async verifyAuthProof(
|
||||
proof: AuthProof,
|
||||
challenge: AuthChallenge,
|
||||
publicKey: CryptoKey
|
||||
): Promise<boolean>
|
||||
Verifies authentication proof against challenge.
|
||||
Utility Functions
|
||||
generateSalt()
|
||||
javascriptstatic generateSalt(): number[]
|
||||
Generates 64-byte cryptographically secure salt.
|
||||
sanitizeMessage()
|
||||
javascriptstatic sanitizeMessage(message: string): string
|
||||
Sanitizes user input to prevent XSS attacks.
|
||||
Example:
|
||||
javascriptconst clean = EnhancedSecureCryptoUtils.sanitizeMessage("<script>alert('xss')</script>Hello");
|
||||
// Returns: "Hello"
|
||||
calculateSecurityLevel()
|
||||
javascriptstatic async calculateSecurityLevel(securityManager: any): Promise<SecurityLevel>
|
||||
Calculates real-time security level based on active protections.
|
||||
Returns:
|
||||
typescriptinterface SecurityLevel {
|
||||
level: 'HIGH' | 'MEDIUM' | 'LOW' | 'UNKNOWN';
|
||||
score: number; // 0-100
|
||||
color: 'green' | 'yellow' | 'red';
|
||||
verificationResults: Record<string, VerificationResult>;
|
||||
timestamp: number;
|
||||
details: string;
|
||||
}
|
||||
|
||||
interface VerificationResult {
|
||||
passed: boolean;
|
||||
details: string;
|
||||
}
|
||||
generateVerificationCode()
|
||||
javascriptstatic generateVerificationCode(): string
|
||||
Generates 6-character verification code for out-of-band authentication.
|
||||
Returns: Code in format "AB-CD-EF"
|
||||
calculateKeyFingerprint()
|
||||
javascriptstatic async calculateKeyFingerprint(keyData: number[]): Promise<string>
|
||||
Calculates SHA-256 fingerprint of key data for MITM protection.
|
||||
encryptData() / decryptData()
|
||||
javascriptstatic async encryptData(data: any, password: string): Promise<string>
|
||||
static async decryptData(encryptedData: string, password: string): Promise<any>
|
||||
High-level encryption/decryption for offer/answer exchange.
|
||||
Example:
|
||||
javascriptconst password = EnhancedSecureCryptoUtils.generateSecurePassword();
|
||||
const encrypted = await EnhancedSecureCryptoUtils.encryptData(
|
||||
{ message: "secret data" },
|
||||
password
|
||||
);
|
||||
const decrypted = await EnhancedSecureCryptoUtils.decryptData(encrypted, password);
|
||||
🌐 EnhancedSecureWebRTCManager
|
||||
Manages P2P connections with enhanced security features.
|
||||
Constructor
|
||||
javascriptnew EnhancedSecureWebRTCManager(
|
||||
onMessage: (message: string, type: string) => void,
|
||||
onStatusChange: (status: string) => void,
|
||||
onKeyExchange: (fingerprint: string) => void,
|
||||
onVerificationRequired: (code: string) => void
|
||||
)
|
||||
Parameters:
|
||||
|
||||
onMessage - Callback for received messages
|
||||
onStatusChange - Callback for connection state changes
|
||||
onKeyExchange - Callback when keys are exchanged
|
||||
onVerificationRequired - Callback when verification code is generated
|
||||
|
||||
Connection Management
|
||||
createSecureOffer()
|
||||
javascriptasync createSecureOffer(): Promise<SecureOffer>
|
||||
Creates encrypted connection offer with ECDH keys and authentication.
|
||||
Returns:
|
||||
typescriptinterface SecureOffer {
|
||||
type: 'enhanced_secure_offer';
|
||||
sdp: string;
|
||||
ecdhPublicKey: SignedPublicKey;
|
||||
ecdsaPublicKey: SignedPublicKey;
|
||||
salt: number[];
|
||||
verificationCode: string;
|
||||
authChallenge: AuthChallenge;
|
||||
sessionId: string;
|
||||
timestamp: number;
|
||||
version: string;
|
||||
securityLevel: SecurityLevel;
|
||||
}
|
||||
|
||||
interface SignedPublicKey {
|
||||
keyType: 'ECDH' | 'ECDSA';
|
||||
keyData: number[];
|
||||
timestamp: number;
|
||||
version: string;
|
||||
signature: number[];
|
||||
}
|
||||
Example:
|
||||
javascriptconst webrtcManager = new EnhancedSecureWebRTCManager(/*...*/);
|
||||
const offer = await webrtcManager.createSecureOffer();
|
||||
console.log('Verification code:', offer.verificationCode);
|
||||
createSecureAnswer()
|
||||
javascriptasync createSecureAnswer(offerData: SecureOffer): Promise<SecureAnswer>
|
||||
Creates encrypted response to connection offer.
|
||||
Returns:
|
||||
typescriptinterface SecureAnswer {
|
||||
type: 'enhanced_secure_answer';
|
||||
sdp: string;
|
||||
ecdhPublicKey: SignedPublicKey;
|
||||
ecdsaPublicKey: SignedPublicKey;
|
||||
authProof: AuthProof;
|
||||
timestamp: number;
|
||||
version: string;
|
||||
securityLevel: SecurityLevel;
|
||||
}
|
||||
handleSecureAnswer()
|
||||
javascriptasync handleSecureAnswer(answerData: SecureAnswer): Promise<void>
|
||||
Processes encrypted answer and establishes connection.
|
||||
Throws: Error if answer is invalid or authentication fails
|
||||
Message Handling
|
||||
sendSecureMessage()
|
||||
javascriptasync sendSecureMessage(message: string): Promise<void>
|
||||
Sends encrypted message through secure channel.
|
||||
Parameters:
|
||||
|
||||
message - Plaintext message (auto-sanitized)
|
||||
|
||||
Features:
|
||||
|
||||
Automatic encryption with metadata protection
|
||||
Sequence number tracking
|
||||
Rate limiting (60 messages/minute)
|
||||
Perfect Forward Secrecy key rotation
|
||||
|
||||
Example:
|
||||
javascriptawait webrtcManager.sendSecureMessage("Hello, secure world!");
|
||||
Connection States
|
||||
typescripttype ConnectionState =
|
||||
| 'disconnected' // No connection
|
||||
| 'connecting' // Establishing connection
|
||||
| 'verifying' // Verifying security codes
|
||||
| 'connected' // Fully connected and verified
|
||||
| 'failed' // Connection failed
|
||||
| 'reconnecting' // Attempting to reconnect
|
||||
| 'peer_disconnected'; // Peer disconnected
|
||||
Security Features
|
||||
calculateSecurityLevel()
|
||||
javascriptasync calculateSecurityLevel(): Promise<SecurityLevel>
|
||||
Real-time security assessment with verification of:
|
||||
|
||||
✅ Encryption functionality
|
||||
✅ ECDH key exchange
|
||||
✅ ECDSA signatures
|
||||
✅ Mutual authentication
|
||||
✅ Metadata protection
|
||||
✅ Replay protection
|
||||
✅ Non-extractable keys
|
||||
✅ Rate limiting
|
||||
✅ Perfect Forward Secrecy
|
||||
|
||||
shouldRotateKeys()
|
||||
javascriptshouldRotateKeys(): boolean
|
||||
Determines if PFS key rotation is needed (every 5 minutes or 100 messages).
|
||||
isConnected()
|
||||
javascriptisConnected(): boolean
|
||||
Returns true if WebRTC data channel is open and ready.
|
||||
getConnectionInfo()
|
||||
javascriptgetConnectionInfo(): ConnectionInfo
|
||||
Returns:
|
||||
typescriptinterface ConnectionInfo {
|
||||
fingerprint: string;
|
||||
isConnected: boolean;
|
||||
isVerified: boolean;
|
||||
connectionState: string;
|
||||
iceConnectionState: string;
|
||||
verificationCode: string;
|
||||
}
|
||||
Perfect Forward Secrecy
|
||||
rotateKeys()
|
||||
javascriptasync rotateKeys(): Promise<boolean>
|
||||
Performs key rotation for Perfect Forward Secrecy.
|
||||
Returns: true if rotation successful
|
||||
getKeysForVersion()
|
||||
javascriptgetKeysForVersion(version: number): KeySet | null
|
||||
Retrieves keys for specific version (for decrypting old messages).
|
||||
Returns:
|
||||
typescriptinterface KeySet {
|
||||
encryptionKey: CryptoKey;
|
||||
macKey: CryptoKey;
|
||||
metadataKey: CryptoKey;
|
||||
}
|
||||
Connection Control
|
||||
disconnect()
|
||||
javascriptdisconnect(): void
|
||||
Cleanly disconnects and cleans up all resources.
|
||||
confirmVerification()
|
||||
javascriptconfirmVerification(): void
|
||||
Confirms that verification codes match (called after manual verification).
|
||||
⚡ PayPerSessionManager
|
||||
Handles Lightning Network payment integration.
|
||||
Constructor
|
||||
javascriptnew PayPerSessionManager()
|
||||
Session Types
|
||||
typescriptinterface SessionPricing {
|
||||
free: { sats: 0, hours: 1/60, usd: 0.00 };
|
||||
basic: { sats: 500, hours: 1, usd: 0.20 };
|
||||
premium: { sats: 1000, hours: 4, usd: 0.40 };
|
||||
extended: { sats: 2000, hours: 24, usd: 0.80 };
|
||||
}
|
||||
Payment Methods
|
||||
createInvoice()
|
||||
javascriptcreateInvoice(sessionType: string): LightningInvoice
|
||||
Creates Lightning invoice for session payment.
|
||||
Parameters:
|
||||
|
||||
sessionType - One of: 'free', 'basic', 'premium', 'extended'
|
||||
|
||||
Returns:
|
||||
typescriptinterface LightningInvoice {
|
||||
amount: number; // satoshis
|
||||
memo: string;
|
||||
sessionType: string;
|
||||
timestamp: number;
|
||||
paymentHash: string;
|
||||
lightningAddress: string;
|
||||
}
|
||||
Example:
|
||||
javascriptconst sessionManager = new PayPerSessionManager();
|
||||
const invoice = sessionManager.createInvoice('premium');
|
||||
console.log(`Pay ${invoice.amount} sats to ${invoice.lightningAddress}`);
|
||||
verifyPayment()
|
||||
javascriptasync verifyPayment(preimage: string, paymentHash: string): Promise<boolean>
|
||||
Verifies Lightning payment preimage.
|
||||
Parameters:
|
||||
|
||||
preimage - Payment preimage (64 hex characters)
|
||||
paymentHash - Payment hash from invoice
|
||||
|
||||
Returns: true if payment is valid
|
||||
activateSession()
|
||||
javascriptactivateSession(sessionType: string, preimage: string): Session
|
||||
Activates paid session.
|
||||
Returns:
|
||||
typescriptinterface Session {
|
||||
type: string;
|
||||
startTime: number;
|
||||
expiresAt: number;
|
||||
preimage: string;
|
||||
}
|
||||
Session Management
|
||||
hasActiveSession()
|
||||
javascripthasActiveSession(): boolean
|
||||
Returns true if there's an active, non-expired session.
|
||||
getTimeLeft()
|
||||
javascriptgetTimeLeft(): number
|
||||
Returns milliseconds remaining in current session.
|
||||
Example:
|
||||
javascriptconst timeLeft = sessionManager.getTimeLeft();
|
||||
const hoursLeft = Math.floor(timeLeft / (1000 * 60 * 60));
|
||||
console.log(`${hoursLeft} hours remaining`);
|
||||
cleanup()
|
||||
javascriptcleanup(): void
|
||||
Cleans up session data and timers.
|
||||
🔧 Integration Examples
|
||||
Basic P2P Chat Setup
|
||||
javascript// Initialize WebRTC manager
|
||||
const webrtcManager = new EnhancedSecureWebRTCManager(
|
||||
(message, type) => {
|
||||
console.log(`${type}: ${message}`);
|
||||
addMessageToUI(message, type);
|
||||
},
|
||||
(status) => {
|
||||
console.log(`Status: ${status}`);
|
||||
updateStatusIndicator(status);
|
||||
},
|
||||
(fingerprint) => {
|
||||
console.log(`Key fingerprint: ${fingerprint}`);
|
||||
displayFingerprint(fingerprint);
|
||||
},
|
||||
(code) => {
|
||||
console.log(`Verification code: ${code}`);
|
||||
showVerificationModal(code);
|
||||
}
|
||||
);
|
||||
|
||||
// Create secure offer
|
||||
const offer = await webrtcManager.createSecureOffer();
|
||||
console.log('Share this encrypted offer:', JSON.stringify(offer));
|
||||
|
||||
// Send message (after connection established)
|
||||
await webrtcManager.sendSecureMessage('Hello, secure world!');
|
||||
Lightning Payment Integration
|
||||
javascript// Initialize session manager
|
||||
const sessionManager = new PayPerSessionManager();
|
||||
|
||||
// Create invoice for premium session
|
||||
const invoice = sessionManager.createInvoice('premium');
|
||||
console.log(`Pay ${invoice.amount} sats to: ${invoice.lightningAddress}`);
|
||||
|
||||
// Handle payment (WebLN)
|
||||
if (window.webln) {
|
||||
try {
|
||||
await window.webln.enable();
|
||||
const result = await window.webln.sendPayment({
|
||||
amount: invoice.amount,
|
||||
memo: invoice.memo
|
||||
});
|
||||
|
||||
// Verify and activate session
|
||||
const isValid = await sessionManager.verifyPayment(
|
||||
result.preimage,
|
||||
invoice.paymentHash
|
||||
);
|
||||
|
||||
if (isValid) {
|
||||
const session = sessionManager.activateSession('premium', result.preimage);
|
||||
console.log(`Session active until: ${new Date(session.expiresAt)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebLN payment failed:', error);
|
||||
}
|
||||
}
|
||||
Custom Cryptographic Operations
|
||||
javascript// Generate fresh key pairs
|
||||
const ecdhKeys = await EnhancedSecureCryptoUtils.generateECDHKeyPair();
|
||||
const ecdsaKeys = await EnhancedSecureCryptoUtils.generateECDSAKeyPair();
|
||||
|
||||
// Create and verify signature
|
||||
const data = 'Important message to sign';
|
||||
const signature = await EnhancedSecureCryptoUtils.signData(
|
||||
ecdsaKeys.privateKey,
|
||||
data
|
||||
);
|
||||
|
||||
const isValid = await EnhancedSecureCryptoUtils.verifySignature(
|
||||
ecdsaKeys.publicKey,
|
||||
signature,
|
||||
data
|
||||
);
|
||||
console.log('Signature valid:', isValid);
|
||||
|
||||
// Derive shared keys
|
||||
const salt = EnhancedSecureCryptoUtils.generateSalt();
|
||||
const sharedKeys = await EnhancedSecureCryptoUtils.deriveSharedKeys(
|
||||
ecdhKeys.privateKey,
|
||||
remotePublicKey,
|
||||
salt
|
||||
);
|
||||
|
||||
// Encrypt message
|
||||
const encrypted = await EnhancedSecureCryptoUtils.encryptMessage(
|
||||
"Secret message",
|
||||
sharedKeys.encryptionKey,
|
||||
sharedKeys.macKey,
|
||||
sharedKeys.metadataKey,
|
||||
"msg_001",
|
||||
1
|
||||
);
|
||||
Full Connection Flow
|
||||
javascript// Complete initiator flow
|
||||
async function initiatorFlow() {
|
||||
// 1. Create WebRTC manager
|
||||
const manager = new EnhancedSecureWebRTCManager(
|
||||
handleMessage,
|
||||
handleStatusChange,
|
||||
handleKeyExchange,
|
||||
handleVerification
|
||||
);
|
||||
|
||||
// 2. Create offer
|
||||
const offer = await manager.createSecureOffer();
|
||||
|
||||
// 3. Encrypt offer for sharing
|
||||
const password = EnhancedSecureCryptoUtils.generateSecurePassword();
|
||||
const encryptedOffer = await EnhancedSecureCryptoUtils.encryptData(offer, password);
|
||||
|
||||
// 4. Share encrypted offer and password with peer
|
||||
console.log('Encrypted offer:', encryptedOffer);
|
||||
console.log('Password:', password);
|
||||
|
||||
// 5. Wait for encrypted answer from peer
|
||||
const encryptedAnswer = await getAnswerFromPeer();
|
||||
const answerPassword = await getPasswordFromPeer();
|
||||
|
||||
// 6. Decrypt and process answer
|
||||
const answer = await EnhancedSecureCryptoUtils.decryptData(
|
||||
encryptedAnswer,
|
||||
answerPassword
|
||||
);
|
||||
await manager.handleSecureAnswer(answer);
|
||||
|
||||
// 7. Verify out-of-band codes
|
||||
await verifySecurityCodes();
|
||||
|
||||
// 8. Start secure communication
|
||||
await manager.sendSecureMessage("Hello from initiator!");
|
||||
}
|
||||
Responder Flow
|
||||
javascriptasync function responderFlow() {
|
||||
// 1. Get encrypted offer from initiator
|
||||
const encryptedOffer = await getOfferFromPeer();
|
||||
const offerPassword = await getPasswordFromPeer();
|
||||
|
||||
// 2. Decrypt offer
|
||||
const offer = await EnhancedSecureCryptoUtils.decryptData(
|
||||
encryptedOffer,
|
||||
offerPassword
|
||||
);
|
||||
|
||||
// 3. Create WebRTC manager
|
||||
const manager = new EnhancedSecureWebRTCManager(
|
||||
handleMessage,
|
||||
handleStatusChange,
|
||||
handleKeyExchange,
|
||||
handleVerification
|
||||
);
|
||||
|
||||
// 4. Create answer
|
||||
const answer = await manager.createSecureAnswer(offer);
|
||||
|
||||
// 5. Encrypt answer for sharing
|
||||
const password = EnhancedSecureCryptoUtils.generateSecurePassword();
|
||||
const encryptedAnswer = await EnhancedSecureCryptoUtils.encryptData(answer, password);
|
||||
|
||||
// 6. Share encrypted answer and password
|
||||
await sendAnswerToPeer(encryptedAnswer);
|
||||
await sendPasswordToPeer(password);
|
||||
|
||||
// 7. Verify out-of-band codes
|
||||
await verifySecurityCodes();
|
||||
|
||||
// 8. Start secure communication
|
||||
await manager.sendSecureMessage("Hello from responder!");
|
||||
}
|
||||
🔒 Security Considerations
|
||||
Key Security
|
||||
|
||||
All keys are non-extractable - Cannot be exported from WebCrypto
|
||||
Hardware security module - Keys protected by browser's HSM
|
||||
Perfect Forward Secrecy - Old messages stay secure even if current keys compromised
|
||||
Automatic key rotation - Keys change every 5 minutes
|
||||
|
||||
Message Security
|
||||
|
||||
Authenticated encryption - AES-GCM provides confidentiality + integrity
|
||||
Metadata protection - Message metadata separately encrypted
|
||||
Replay protection - Sequence numbers prevent message replay
|
||||
Rate limiting - Prevents spam and DoS attacks
|
||||
|
||||
Connection Security
|
||||
|
||||
Out-of-band verification - Manual code verification prevents MITM
|
||||
Mutual authentication - Both parties prove identity
|
||||
Direct P2P - No intermediate servers to compromise
|
||||
WebRTC encryption - DTLS transport layer security
|
||||
|
||||
Payment Security
|
||||
|
||||
Lightning Network - No credit card or banking data exposure
|
||||
Preimage verification - Cryptographic proof of payment
|
||||
No payment data stored - Payments verified and discarded
|
||||
|
||||
🐛 Error Handling
|
||||
Common Error Types
|
||||
typescript// Cryptographic errors
|
||||
class CryptoError extends Error {
|
||||
constructor(message: string) {
|
||||
super(`Crypto Error: ${message}`);
|
||||
this.name = 'CryptoError';
|
||||
}
|
||||
}
|
||||
|
||||
// Connection errors
|
||||
class ConnectionError extends Error {
|
||||
constructor(message: string) {
|
||||
super(`Connection Error: ${message}`);
|
||||
this.name = 'ConnectionError';
|
||||
}
|
||||
}
|
||||
|
||||
// Payment errors
|
||||
class PaymentError extends Error {
|
||||
constructor(message: string) {
|
||||
super(`Payment Error: ${message}`);
|
||||
this.name = 'PaymentError';
|
||||
}
|
||||
}
|
||||
Error Recovery Patterns
|
||||
javascript// Robust message sending with retry
|
||||
async function sendMessageWithRetry(manager, message, maxRetries = 3) {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
await manager.sendSecureMessage(message);
|
||||
return; // Success
|
||||
} catch (error) {
|
||||
console.warn(`Send attempt ${attempt} failed:`, error.message);
|
||||
|
||||
if (error.message.includes('Session expired')) {
|
||||
throw new PaymentError('Session expired - payment required');
|
||||
}
|
||||
|
||||
if (error.message.includes('Rate limit')) {
|
||||
// Wait before retry
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
throw error; // Final attempt failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connection error handling
|
||||
function handleConnectionError(error) {
|
||||
if (error.message.includes('MITM')) {
|
||||
alert('⚠️ Security threat detected! Connection terminated.');
|
||||
return 'security_threat';
|
||||
}
|
||||
|
||||
if (error.message.includes('timeout')) {
|
||||
return 'timeout';
|
||||
}
|
||||
|
||||
if (error.message.includes('ice')) {
|
||||
return 'nat_traversal';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// Payment error handling
|
||||
function handlePaymentError(error) {
|
||||
if (error.message.includes('preimage')) {
|
||||
return 'invalid_payment';
|
||||
}
|
||||
|
||||
if (error.message.includes('expired')) {
|
||||
return 'session_expired';
|
||||
}
|
||||
|
||||
if (error.message.includes('webln')) {
|
||||
return 'webln_failed';
|
||||
}
|
||||
|
||||
return 'payment_failed';
|
||||
}
|
||||
🧪 Testing
|
||||
Unit Testing Examples
|
||||
javascript// Test encryption/decryption round-trip
|
||||
async function testEncryptionRoundTrip() {
|
||||
const originalMessage = 'Test message for encryption';
|
||||
const keys = await generateTestKeys();
|
||||
|
||||
const encrypted = await EnhancedSecureCryptoUtils.encryptMessage(
|
||||
originalMessage,
|
||||
keys.encryptionKey,
|
||||
keys.macKey,
|
||||
keys.metadataKey,
|
||||
'test-id',
|
||||
0
|
||||
);
|
||||
|
||||
const decrypted = await EnhancedSecureCryptoUtils.decryptMessage(
|
||||
encrypted,
|
||||
keys.encryptionKey,
|
||||
keys.macKey,
|
||||
keys.metadataKey
|
||||
);
|
||||
|
||||
assert.equal(decrypted.message, originalMessage);
|
||||
assert.equal(decrypted.messageId, 'test-id');
|
||||
assert.equal(decrypted.sequenceNumber, 0);
|
||||
}
|
||||
|
||||
// Test key generation
|
||||
async function testKeyGeneration() {
|
||||
const ecdhPair = await EnhancedSecureCryptoUtils.generateECDHKeyPair();
|
||||
const ecdsaPair = await EnhancedSecureCryptoUtils.generateECDSAKeyPair();
|
||||
|
||||
// Verify key properties
|
||||
assert.equal(ecdhPair.privateKey.algorithm.name, 'ECDH');
|
||||
assert.equal(ecdhPair.privateKey.algorithm.namedCurve, 'P-384');
|
||||
assert.equal(ecdhPair.privateKey.extractable, false);
|
||||
|
||||
assert.equal(ecdsaPair.privateKey.algorithm.name, 'ECDSA');
|
||||
assert.equal(ecdsaPair.privateKey.algorithm.namedCurve, 'P-384');
|
||||
assert.equal(ecdsaPair.privateKey.extractable, false);
|
||||
}
|
||||
|
||||
// Test signature verification
|
||||
async function testSignatureVerification() {
|
||||
const keyPair = await EnhancedSecureCryptoUtils.generateECDSAKeyPair();
|
||||
const data = 'Test data to sign';
|
||||
|
||||
const signature = await EnhancedSecureCryptoUtils.signData(
|
||||
keyPair.privateKey,
|
||||
data
|
||||
);
|
||||
|
||||
const isValid = await EnhancedSecureCryptoUtils.verifySignature(
|
||||
keyPair.publicKey,
|
||||
signature,
|
||||
data
|
||||
);
|
||||
|
||||
assert.equal(isValid, true);
|
||||
|
||||
// Test with wrong data
|
||||
const invalidVerification = await EnhancedSecureCryptoUtils.verifySignature(
|
||||
keyPair.publicKey,
|
||||
signature,
|
||||
'Wrong data'
|
||||
);
|
||||
|
||||
assert.equal(invalidVerification, false);
|
||||
}
|
||||
|
||||
// Helper function for tests
|
||||
async function generateTestKeys() {
|
||||
const ecdhPair = await EnhancedSecureCryptoUtils.generateECDHKeyPair();
|
||||
const salt = EnhancedSecureCryptoUtils.generateSalt();
|
||||
|
||||
// For testing, we'll create a mock "remote" key pair
|
||||
const remotePair = await EnhancedSecureCryptoUtils.generateECDHKeyPair();
|
||||
|
||||
const sharedKeys = await EnhancedSecureCryptoUtils.deriveSharedKeys(
|
||||
ecdhPair.privateKey,
|
||||
remotePair.publicKey,
|
||||
salt
|
||||
);
|
||||
|
||||
return sharedKeys;
|
||||
}
|
||||
Integration Testing
|
||||
javascript// Test full P2P connection flow
|
||||
async function testP2PConnection() {
|
||||
let manager1Messages = [];
|
||||
let manager2Messages = [];
|
||||
|
||||
const manager1 = new EnhancedSecureWebRTCManager(
|
||||
(msg, type) => manager1Messages.push({msg, type}),
|
||||
(status) => console.log('Manager1 status:', status),
|
||||
(fingerprint) => console.log('Manager1 fingerprint:', fingerprint),
|
||||
(code) => console.log('Manager1 verification:', code)
|
||||
);
|
||||
|
||||
const manager2 = new EnhancedSecureWebRTCManager(
|
||||
(msg, type) => manager2Messages.push({msg, type}),
|
||||
(status) => console.log('Manager2 status:', status),
|
||||
(fingerprint) => console.log('Manager2 fingerprint:', fingerprint),
|
||||
(code) => console.log('Manager2 verification:', code)
|
||||
);
|
||||
|
||||
// Create offer
|
||||
const offer = await manager1.createSecureOffer();
|
||||
|
||||
// Create answer
|
||||
const answer = await manager2.createSecureAnswer(offer);
|
||||
|
||||
// Handle answer
|
||||
await manager1.handleSecureAnswer(answer);
|
||||
|
||||
// Wait for connection
|
||||
await waitForConnection(manager1, manager2);
|
||||
|
||||
// Verify both are connected
|
||||
assert.equal(manager1.isConnected(), true);
|
||||
assert.equal(manager2.isConnected(), true);
|
||||
|
||||
// Test message exchange
|
||||
await manager1.sendSecureMessage('Hello from manager1');
|
||||
await manager2.sendSecureMessage('Hello from manager2');
|
||||
|
||||
// Wait for messages to arrive
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Verify messages were received
|
||||
assert.equal(manager2Messages.length > 0, true);
|
||||
assert.equal(manager1Messages.length > 0, true);
|
||||
}
|
||||
|
||||
async function waitForConnection(manager1, manager2, timeout = 10000) {
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
if (manager1.isConnected() && manager2.isConnected()) {
|
||||
return;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
throw new Error('Connection timeout');
|
||||
}
|
||||
1224
doc/CONTRIBUTING.md
Normal file
1360
doc/CRYPTOGRAPHY.md
Normal file
720
doc/SECURITY-ARCHITECTURE.md
Normal file
@@ -0,0 +1,720 @@
|
||||
# SecureBit.chat Security Architecture
|
||||
|
||||
## 🛡️ Overview
|
||||
|
||||
SecureBit.chat implements a revolutionary **12-layer security architecture** that provides military-grade protection for peer-to-peer communications. This document details the technical implementation of our security system, which exceeds most government and enterprise communication standards.
|
||||
|
||||
**Current Implementation:** Stage 4 - Maximum Security
|
||||
**Security Rating:** Military-Grade
|
||||
**Active Layers:** 12/12
|
||||
**Threat Protection:** Comprehensive (MITM, Traffic Analysis, Replay Attacks, Session Hijacking)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Security Architecture Overview](#security-architecture-overview)
|
||||
2. [Layer-by-Layer Analysis](#layer-by-layer-analysis)
|
||||
3. [Cryptographic Specifications](#cryptographic-specifications)
|
||||
4. [Threat Model](#threat-model)
|
||||
5. [Implementation Details](#implementation-details)
|
||||
6. [Security Verification](#security-verification)
|
||||
7. [Performance Impact](#performance-impact)
|
||||
8. [Compliance Standards](#compliance-standards)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Security Architecture Overview
|
||||
|
||||
### 12-Layer Defense System
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ APPLICATION LAYER │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Layer 12: Perfect Forward Secrecy (Key Rotation) │
|
||||
│ Layer 11: Enhanced Rate Limiting (DDoS Protection) │
|
||||
│ Layer 10: Fake Traffic Generation (Traffic Analysis) │
|
||||
│ Layer 9: Message Chunking (Timing Analysis Protection) │
|
||||
│ Layer 8: Packet Reordering Protection (Sequence Security) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ OBFUSCATION LAYER │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Layer 7: Anti-Fingerprinting (Pattern Obfuscation) │
|
||||
│ Layer 6: Packet Padding (Size Obfuscation) │
|
||||
│ Layer 5: Nested Encryption (Additional AES-GCM) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ENCRYPTION LAYER │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Layer 4: Message Encryption (Enhanced AES-GCM) │
|
||||
│ Layer 3: Metadata Protection (Separate AES-GCM) │
|
||||
│ Layer 2: Key Exchange (ECDH P-384) │
|
||||
│ Layer 1: Enhanced Authentication (ECDSA P-384) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ TRANSPORT LAYER │
|
||||
│ (WebRTC/ICE/DTLS) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Security Progression Stages
|
||||
|
||||
| Stage | Layers Active | Security Level | Target Threats |
|
||||
|-------|---------------|----------------|-----------------|
|
||||
| 1 | 1-5 | Basic Enhanced | Basic attacks, MITM |
|
||||
| 2 | 1-7 | Medium | + Traffic analysis |
|
||||
| 3 | 1-9 | High | + Timing attacks |
|
||||
| 4 | 1-12 | Maximum | + Advanced persistent threats |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Layer-by-Layer Analysis
|
||||
|
||||
### Layer 1: Enhanced Authentication (ECDSA P-384)
|
||||
**Purpose:** Cryptographic proof of message authenticity and sender verification
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Algorithm:** ECDSA with P-384 curve
|
||||
- **Hash Function:** SHA-384 (primary), SHA-256 (fallback)
|
||||
- **Key Size:** 384-bit (equivalent to 7680-bit RSA)
|
||||
- **Signature Size:** 96 bytes
|
||||
- **Key Properties:** Non-extractable, hardware-protected
|
||||
|
||||
**Implementation:**
|
||||
```javascript
|
||||
// Self-signed key package for MITM protection
|
||||
const keyPackage = {
|
||||
keyType: 'ECDSA',
|
||||
keyData: exported384BitKey,
|
||||
timestamp: Date.now(),
|
||||
version: '4.0',
|
||||
signature: ecdsaSignature
|
||||
};
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Message tampering
|
||||
- Sender impersonation
|
||||
- Man-in-the-middle attacks
|
||||
- Key substitution attacks
|
||||
|
||||
---
|
||||
|
||||
### Layer 2: Key Exchange (ECDH P-384)
|
||||
**Purpose:** Secure key agreement between peers without central authority
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Algorithm:** Elliptic Curve Diffie-Hellman
|
||||
- **Curve:** NIST P-384 (secp384r1)
|
||||
- **Key Derivation:** HKDF with SHA-384
|
||||
- **Salt Size:** 64 bytes (enhanced from standard 32 bytes)
|
||||
- **Context Info:** "SecureBit.chat v4.0 Enhanced Security Edition"
|
||||
|
||||
**Key Derivation Process:**
|
||||
```javascript
|
||||
// Triple key derivation for maximum security
|
||||
const derivedKeys = {
|
||||
encryptionKey: HKDF(sharedSecret, salt, "message-encryption-v4"),
|
||||
macKey: HKDF(sharedSecret, salt, "message-authentication-v4"),
|
||||
metadataKey: HKDF(sharedSecret, salt, "metadata-protection-v4")
|
||||
};
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Passive eavesdropping
|
||||
- Key recovery attacks
|
||||
- Weak key generation
|
||||
- Quantum computer threats (post-quantum resistant)
|
||||
|
||||
---
|
||||
|
||||
### Layer 3: Metadata Protection (Separate AES-GCM)
|
||||
**Purpose:** Protect message metadata from analysis and correlation
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Algorithm:** AES-256-GCM
|
||||
- **Key:** Separate 256-bit key derived from ECDH
|
||||
- **IV:** 96-bit random per message
|
||||
- **Authentication:** Integrated GMAC
|
||||
- **Protected Data:** Message ID, timestamp, sequence number, original length
|
||||
|
||||
**Metadata Structure:**
|
||||
```javascript
|
||||
const protectedMetadata = {
|
||||
id: "msg_timestamp_counter",
|
||||
timestamp: encryptedTimestamp,
|
||||
sequenceNumber: encryptedSequence,
|
||||
originalLength: encryptedLength,
|
||||
version: "4.0"
|
||||
};
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Traffic flow analysis
|
||||
- Message correlation attacks
|
||||
- Timing analysis
|
||||
- Size-based fingerprinting
|
||||
|
||||
---
|
||||
|
||||
### Layer 4: Message Encryption (Enhanced AES-GCM)
|
||||
**Purpose:** Primary message content protection with authenticated encryption
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Algorithm:** AES-256-GCM
|
||||
- **Key:** 256-bit derived from ECDH
|
||||
- **IV:** 96-bit random per message
|
||||
- **Authentication:** Integrated GMAC + separate HMAC
|
||||
- **Padding:** PKCS#7 + random padding
|
||||
- **MAC Algorithm:** HMAC-SHA-384
|
||||
|
||||
**Enhanced Features:**
|
||||
- Sequence number validation
|
||||
- Replay attack prevention
|
||||
- Message integrity verification
|
||||
- Deterministic serialization for MAC
|
||||
|
||||
**Protection Against:**
|
||||
- Content interception
|
||||
- Message modification
|
||||
- Replay attacks
|
||||
- Authentication bypass
|
||||
|
||||
---
|
||||
|
||||
### Layer 5: Nested Encryption (Additional AES-GCM)
|
||||
**Purpose:** Second layer of encryption for maximum confidentiality
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Algorithm:** AES-256-GCM (independent instance)
|
||||
- **Key:** Separate 256-bit key (hardware-generated)
|
||||
- **IV:** 96-bit unique per encryption
|
||||
- **Counter:** Incremental counter for IV uniqueness
|
||||
- **Key Rotation:** Every 1000 messages or 15 minutes
|
||||
|
||||
**Implementation:**
|
||||
```javascript
|
||||
// Nested encryption with unique IV
|
||||
const uniqueIV = new Uint8Array(12);
|
||||
uniqueIV.set(baseIV);
|
||||
uniqueIV[11] = (counter++) & 0xFF;
|
||||
|
||||
const nestedEncrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: uniqueIV },
|
||||
nestedEncryptionKey,
|
||||
alreadyEncryptedData
|
||||
);
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Cryptographic implementation flaws
|
||||
- Algorithm-specific attacks
|
||||
- Side-channel attacks
|
||||
- Future cryptographic breaks
|
||||
|
||||
---
|
||||
|
||||
### Layer 6: Packet Padding (Size Obfuscation)
|
||||
**Purpose:** Hide real message sizes to prevent traffic analysis
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Padding Range:** 64-1024 bytes (configurable)
|
||||
- **Algorithm:** Cryptographically secure random
|
||||
- **Distribution:** Uniform random within range
|
||||
- **Header:** 4-byte original size indicator
|
||||
- **Efficiency:** Optimized for minimal overhead
|
||||
|
||||
**Padding Algorithm:**
|
||||
```javascript
|
||||
const paddingSize = Math.floor(Math.random() *
|
||||
(maxPadding - minPadding + 1)) + minPadding;
|
||||
const padding = crypto.getRandomValues(new Uint8Array(paddingSize));
|
||||
|
||||
// Structure: [originalSize:4][originalData][randomPadding]
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Message size analysis
|
||||
- Traffic pattern recognition
|
||||
- Statistical correlation attacks
|
||||
- Content-based fingerprinting
|
||||
|
||||
---
|
||||
|
||||
### Layer 7: Anti-Fingerprinting (Pattern Obfuscation)
|
||||
**Purpose:** Prevent behavioral analysis and traffic fingerprinting
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Noise Injection:** 8-40 bytes random data
|
||||
- **Size Randomization:** ±25% size variation
|
||||
- **Pattern Masking:** XOR with cryptographic noise
|
||||
- **Header Randomization:** Fake headers injection
|
||||
- **Timing Obfuscation:** Random delays (50-1000ms)
|
||||
|
||||
**Obfuscation Techniques:**
|
||||
```javascript
|
||||
// Multi-layer obfuscation
|
||||
const obfuscated = {
|
||||
addNoise: () => injectRandomBytes(8, 40),
|
||||
randomizeSize: () => varySize(0.75, 1.25),
|
||||
maskPatterns: () => xorWithNoise(data),
|
||||
addFakeHeaders: () => injectFakeHeaders(1, 3)
|
||||
};
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Behavioral fingerprinting
|
||||
- Machine learning classification
|
||||
- Protocol identification
|
||||
- Application detection
|
||||
|
||||
---
|
||||
|
||||
### Layer 8: Packet Reordering Protection (Sequence Security)
|
||||
**Purpose:** Maintain message integrity despite network reordering
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Sequence Numbers:** 32-bit incremental
|
||||
- **Timestamps:** 32-bit Unix timestamp
|
||||
- **Buffer Size:** Maximum 10 out-of-order packets
|
||||
- **Timeout:** 5 seconds for reordering
|
||||
- **Header Size:** 8-12 bytes (depending on configuration)
|
||||
|
||||
**Reordering Algorithm:**
|
||||
```javascript
|
||||
// Packet structure: [sequence:4][timestamp:4][size:4][data]
|
||||
const packetHeader = {
|
||||
sequence: sequenceNumber++,
|
||||
timestamp: Date.now(),
|
||||
dataSize: actualDataLength
|
||||
};
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Packet injection attacks
|
||||
- Sequence number attacks
|
||||
- Network-level tampering
|
||||
- Order-dependent vulnerabilities
|
||||
|
||||
---
|
||||
|
||||
### Layer 9: Message Chunking (Timing Analysis Protection)
|
||||
**Purpose:** Break large messages into randomized chunks with delays
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Chunk Size:** Maximum 1024-2048 bytes
|
||||
- **Delay Range:** 50-300ms between chunks
|
||||
- **Randomization:** True randomness for delays and sizes
|
||||
- **Headers:** 16-byte chunk identification
|
||||
- **Reassembly:** Timeout-based with 5-second limit
|
||||
|
||||
**Chunking Structure:**
|
||||
```javascript
|
||||
// Chunk header: [messageId:4][chunkIndex:4][totalChunks:4][chunkSize:4]
|
||||
const chunkHeader = {
|
||||
messageId: uniqueMessageId,
|
||||
chunkIndex: currentChunk,
|
||||
totalChunks: totalChunkCount,
|
||||
chunkSize: thisChunkSize
|
||||
};
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Timing correlation attacks
|
||||
- Large message identification
|
||||
- Burst analysis
|
||||
- Real-time content analysis
|
||||
|
||||
---
|
||||
|
||||
### Layer 10: Fake Traffic Generation (Traffic Analysis Protection)
|
||||
**Purpose:** Generate convincing decoy traffic to mask real communications
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Frequency:** 10-30 second intervals
|
||||
- **Size Range:** 32-256 bytes
|
||||
- **Patterns:** 5 different message types
|
||||
- **Encryption:** Full security layer processing
|
||||
- **Detection:** Invisible to users (filtered at receiver)
|
||||
|
||||
**Fake Message Types:**
|
||||
```javascript
|
||||
const fakePatterns = {
|
||||
'heartbeat': () => generateHeartbeatPattern(),
|
||||
'status': () => generateStatusPattern(),
|
||||
'sync': () => generateSyncPattern(),
|
||||
'ping': () => generatePingPattern(),
|
||||
'pong': () => generatePongPattern()
|
||||
};
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Traffic volume analysis
|
||||
- Communication timing analysis
|
||||
- Silence period detection
|
||||
- Conversation pattern recognition
|
||||
|
||||
---
|
||||
|
||||
### Layer 11: Enhanced Rate Limiting (DDoS Protection)
|
||||
**Purpose:** Prevent abuse and ensure service availability
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Message Rate:** 60 messages per minute
|
||||
- **Connection Rate:** 5 connections per 5 minutes
|
||||
- **Sliding Window:** Time-based with cleanup
|
||||
- **Verification:** Cryptographic rate tokens
|
||||
- **Storage:** In-memory with automatic cleanup
|
||||
|
||||
**Rate Limiting Algorithm:**
|
||||
```javascript
|
||||
const rateLimits = {
|
||||
messages: new Map(), // identifier -> timestamps[]
|
||||
connections: new Map(), // identifier -> timestamps[]
|
||||
cleanup: () => removeExpiredEntries(1, 'hour')
|
||||
};
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Message flooding attacks
|
||||
- Connection exhaustion
|
||||
- Resource consumption attacks
|
||||
- Service degradation
|
||||
|
||||
---
|
||||
|
||||
### Layer 12: Perfect Forward Secrecy (Key Rotation)
|
||||
**Purpose:** Ensure past communications remain secure even if keys are compromised
|
||||
|
||||
**Technical Specifications:**
|
||||
- **Rotation Interval:** 5 minutes or 100 messages
|
||||
- **Key Versions:** Tracked with version numbers
|
||||
- **Old Key Storage:** Maximum 3 previous versions (15 minutes)
|
||||
- **Rotation Protocol:** Automated with peer coordination
|
||||
- **Cleanup:** Automatic old key destruction
|
||||
|
||||
**Key Rotation Process:**
|
||||
```javascript
|
||||
const pfsImplementation = {
|
||||
rotationTrigger: () => checkTime(5, 'minutes') || checkMessages(100),
|
||||
keyVersioning: () => incrementVersion(),
|
||||
oldKeyCleanup: () => removeKeysOlderThan(15, 'minutes'),
|
||||
automaticRotation: () => rotateIfNeeded()
|
||||
};
|
||||
```
|
||||
|
||||
**Protection Against:**
|
||||
- Long-term key compromise
|
||||
- Historical data decryption
|
||||
- Persistent surveillance
|
||||
- Future cryptographic breaks
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Cryptographic Specifications
|
||||
|
||||
### Algorithm Selection Rationale
|
||||
|
||||
| Component | Algorithm | Key Size | Rationale |
|
||||
|-----------|-----------|----------|-----------|
|
||||
| Key Exchange | ECDH P-384 | 384-bit | NSA Suite B, quantum-resistant timeline |
|
||||
| Signatures | ECDSA P-384 | 384-bit | Matches key exchange, proven security |
|
||||
| Encryption | AES-256-GCM | 256-bit | NIST recommended, authenticated encryption |
|
||||
| Hashing | SHA-384 | 384-bit | Matches curve size, collision resistant |
|
||||
| MAC | HMAC-SHA-384 | 384-bit | Proven security, matches hash function |
|
||||
|
||||
### Security Strengths
|
||||
|
||||
- **ECDH P-384:** Equivalent to 7680-bit RSA
|
||||
- **AES-256:** Quantum computer resistant until 2040+
|
||||
- **SHA-384:** 192-bit security level (collision resistance)
|
||||
- **Combined Security:** Exceeds 256-bit security level
|
||||
|
||||
### Cryptographic Operations Performance
|
||||
|
||||
| Operation | Time (ms) | CPU Usage | Memory Usage |
|
||||
|-----------|-----------|-----------|--------------|
|
||||
| Key Generation | 10-50 | Medium | Low |
|
||||
| ECDH Agreement | 5-15 | Low | Low |
|
||||
| AES Encryption | 0.1-1 | Very Low | Very Low |
|
||||
| ECDSA Signing | 2-8 | Low | Low |
|
||||
| Message Processing | 1-5 | Low | Low |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Threat Model
|
||||
|
||||
### Threat Classifications
|
||||
|
||||
#### **🔴 Critical Threats (Fully Mitigated)**
|
||||
- **Nation-State Attacks:** Advanced persistent threats
|
||||
- **MITM Attacks:** Certificate pinning bypass attempts
|
||||
- **Cryptographic Attacks:** Implementation vulnerabilities
|
||||
- **Traffic Analysis:** Deep packet inspection and metadata analysis
|
||||
|
||||
#### **🟡 High Threats (Substantially Mitigated)**
|
||||
- **Side-Channel Attacks:** Timing and power analysis
|
||||
- **Social Engineering:** User manipulation (partially mitigated)
|
||||
- **Endpoint Compromise:** Device-level attacks
|
||||
- **Quantum Computing:** Future quantum attacks (timeline > 15 years)
|
||||
|
||||
#### **🟢 Medium Threats (Completely Mitigated)**
|
||||
- **Passive Eavesdropping:** Network traffic interception
|
||||
- **Replay Attacks:** Message reuse attempts
|
||||
- **DDoS Attacks:** Service disruption attempts
|
||||
- **Protocol Downgrade:** Forced weak encryption
|
||||
|
||||
### Attack Scenarios and Defenses
|
||||
|
||||
#### Scenario 1: Government Surveillance
|
||||
**Attack:** Comprehensive traffic monitoring and analysis
|
||||
**Defense Layers:** 6, 7, 10, 12 (traffic obfuscation)
|
||||
**Result:** Encrypted traffic indistinguishable from noise
|
||||
|
||||
#### Scenario 2: Corporate Espionage
|
||||
**Attack:** Targeted interception with advanced tools
|
||||
**Defense Layers:** 1, 2, 3, 4, 5 (cryptographic protection)
|
||||
**Result:** Computationally infeasible to decrypt
|
||||
|
||||
#### Scenario 3: ISP-Level Monitoring
|
||||
**Attack:** Deep packet inspection and metadata collection
|
||||
**Defense Layers:** 6, 7, 8, 9, 10 (pattern obfuscation)
|
||||
**Result:** No useful metadata or patterns extractable
|
||||
|
||||
#### Scenario 4: Academic Cryptanalysis
|
||||
**Attack:** Advanced mathematical attacks on crypto
|
||||
**Defense Layers:** 2, 4, 5 (multiple algorithms)
|
||||
**Result:** Multiple independent cryptographic barriers
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Implementation Details
|
||||
|
||||
### Message Flow Through Security Layers
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ User Message │
|
||||
└─────────┬───────┘
|
||||
│
|
||||
┌─────────▼───────┐ ┌──────────────────┐
|
||||
│ Layer 7: Anti │ │ Outbound Process │
|
||||
│ Fingerprinting │◄───┤ (Sending) │
|
||||
└─────────┬───────┘ └──────────────────┘
|
||||
│
|
||||
┌─────────▼───────┐
|
||||
│ Layer 6: Packet│
|
||||
│ Padding │
|
||||
└─────────┬───────┘
|
||||
│
|
||||
┌─────────▼───────┐
|
||||
│ Layer 8: Packet│
|
||||
│ Reordering │
|
||||
└─────────┬───────┘
|
||||
│
|
||||
┌─────────▼───────┐
|
||||
│ Layer 5: Nested│
|
||||
│ Encryption │
|
||||
└─────────┬───────┘
|
||||
│
|
||||
┌─────────▼───────┐
|
||||
│ Layer 4: Message│
|
||||
│ Encryption │
|
||||
└─────────┬───────┘
|
||||
│
|
||||
┌─────────▼───────┐
|
||||
│ WebRTC Channel │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### Security Layer Configuration
|
||||
|
||||
```javascript
|
||||
// Production configuration for maximum security
|
||||
const securityConfig = {
|
||||
// Stage 4 - Maximum Security
|
||||
features: {
|
||||
hasEncryption: true,
|
||||
hasECDH: true,
|
||||
hasECDSA: true,
|
||||
hasMutualAuth: true,
|
||||
hasMetadataProtection: true,
|
||||
hasEnhancedReplayProtection: true,
|
||||
hasNonExtractableKeys: true,
|
||||
hasRateLimiting: true,
|
||||
hasEnhancedValidation: true,
|
||||
hasPFS: true,
|
||||
hasNestedEncryption: true,
|
||||
hasPacketPadding: true,
|
||||
hasFakeTraffic: true,
|
||||
hasMessageChunking: true,
|
||||
hasDecoyChannels: true,
|
||||
hasPacketReordering: true,
|
||||
hasAntiFingerprinting: true
|
||||
},
|
||||
|
||||
// Performance optimizations
|
||||
performance: {
|
||||
paddingRange: [64, 512], // Reduced for efficiency
|
||||
chunkSize: 2048, // Larger chunks
|
||||
fakeTrafficInterval: [10000, 30000], // Less frequent
|
||||
keyRotation: 300000 // 5 minutes
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Security Verification
|
||||
|
||||
### Automated Testing
|
||||
|
||||
```javascript
|
||||
// Security layer verification
|
||||
async function verifySecurityLayers() {
|
||||
const tests = [
|
||||
verifyEncryption(),
|
||||
verifyECDHKeyExchange(),
|
||||
verifyECDSASignatures(),
|
||||
verifyMutualAuth(),
|
||||
verifyMetadataProtection(),
|
||||
verifyReplayProtection(),
|
||||
verifyNonExtractableKeys(),
|
||||
verifyRateLimiting(),
|
||||
verifyEnhancedValidation(),
|
||||
verifyPFS(),
|
||||
verifyNestedEncryption(),
|
||||
verifyPacketPadding()
|
||||
];
|
||||
|
||||
const results = await Promise.all(tests);
|
||||
return results.every(result => result === true);
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Verification Commands
|
||||
|
||||
```javascript
|
||||
// Check security status
|
||||
webrtcManager.getSecurityStatus()
|
||||
// Expected: { stage: 4, securityLevel: 'MAXIMUM', activeFeatures: 12 }
|
||||
|
||||
// Verify cryptographic implementation
|
||||
webrtcManager.calculateSecurityLevel()
|
||||
// Expected: { level: 'HIGH', score: 80+, verificationResults: {...} }
|
||||
|
||||
// Test fake traffic filtering
|
||||
webrtcManager.checkFakeTrafficStatus()
|
||||
// Expected: { fakeTrafficEnabled: true, timerActive: true }
|
||||
```
|
||||
|
||||
### Security Metrics Dashboard
|
||||
|
||||
| Metric | Target | Current | Status |
|
||||
|--------|---------|---------|---------|
|
||||
| Active Security Layers | 12 | 12 | ✅ |
|
||||
| Encryption Strength | 256-bit | 256-bit | ✅ |
|
||||
| Key Exchange Security | P-384 | P-384 | ✅ |
|
||||
| Forward Secrecy | Complete | Complete | ✅ |
|
||||
| Traffic Obfuscation | Maximum | Maximum | ✅ |
|
||||
| Attack Surface | Minimal | Minimal | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Performance Impact
|
||||
|
||||
### Latency Analysis
|
||||
|
||||
| Security Layer | Added Latency | Justification |
|
||||
|----------------|---------------|---------------|
|
||||
| Authentication | ~5ms | Necessary for MITM protection |
|
||||
| Key Exchange | ~10ms | One-time cost per session |
|
||||
| Metadata Protection | ~1ms | Minimal overhead |
|
||||
| Message Encryption | ~2ms | Standard AES-GCM performance |
|
||||
| Nested Encryption | ~2ms | Additional security layer |
|
||||
| Packet Padding | ~0.5ms | Simple padding operation |
|
||||
| Anti-Fingerprinting | ~3ms | Pattern obfuscation |
|
||||
| Reordering Protection | ~1ms | Header processing |
|
||||
| Message Chunking | ~50ms | Intentional delay for security |
|
||||
| Fake Traffic | 0ms | Background operation |
|
||||
| Rate Limiting | ~0.1ms | Memory lookup |
|
||||
| PFS Key Rotation | ~10ms | Every 5 minutes |
|
||||
|
||||
**Total Average Latency:** ~75ms per message (acceptable for secure communications)
|
||||
|
||||
### Throughput Impact
|
||||
|
||||
- **Without Security:** ~1000 messages/second
|
||||
- **With Stage 4 Security:** ~500 messages/second
|
||||
- **Efficiency:** 50% (excellent for security level provided)
|
||||
|
||||
### Memory Usage
|
||||
|
||||
- **Base Memory:** ~2MB
|
||||
- **Security Layers:** ~3MB additional
|
||||
- **Cryptographic Keys:** ~1MB
|
||||
- **Total:** ~6MB (minimal for modern devices)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Compliance Standards
|
||||
|
||||
### Industry Standards Met
|
||||
|
||||
- ✅ **NIST SP 800-57:** Key management best practices
|
||||
- ✅ **FIPS 140-2 Level 2:** Cryptographic module security
|
||||
- ✅ **NSA Suite B:** Cryptographic algorithms (P-384, AES-256)
|
||||
- ✅ **RFC 7748:** Elliptic curve cryptography standards
|
||||
- ✅ **RFC 5869:** HKDF key derivation
|
||||
- ✅ **RFC 3394:** AES key wrap specifications
|
||||
|
||||
### Regulatory Compliance
|
||||
|
||||
- ✅ **GDPR:** Privacy by design, data minimization
|
||||
- ✅ **CCPA:** California privacy protection
|
||||
- ✅ **HIPAA:** Healthcare data protection (suitable for)
|
||||
- ✅ **SOX:** Financial data protection (suitable for)
|
||||
- ✅ **ITAR:** International traffic in arms regulations (crypto export)
|
||||
|
||||
### Security Certifications (Pending)
|
||||
|
||||
- 🔄 **Common Criteria EAL4+:** Security functionality evaluation
|
||||
- 🔄 **FIPS 140-3:** Next-generation cryptographic validation
|
||||
- 🔄 **ISO 27001:** Information security management
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Future Enhancements
|
||||
|
||||
### Quantum-Resistant Cryptography (2026)
|
||||
- **Post-Quantum Key Exchange:** CRYSTALS-Kyber
|
||||
- **Post-Quantum Signatures:** CRYSTALS-Dilithium
|
||||
- **Hybrid Classical/Quantum:** Dual algorithm support
|
||||
|
||||
### Advanced Traffic Obfuscation (2025)
|
||||
- **AI-Powered Pattern Generation:** Machine learning fake traffic
|
||||
- **Protocol Mimicry:** Disguise as common protocols (HTTP, DNS)
|
||||
- **Adaptive Obfuscation:** Real-time pattern adjustment
|
||||
|
||||
### Enhanced Perfect Forward Secrecy (2025)
|
||||
- **Automatic Key Rotation:** Every 1 minute
|
||||
- **Group Key Management:** Multi-party key rotation
|
||||
- **Quantum Key Distribution:** Hardware-based key generation
|
||||
|
||||
---
|
||||
|
||||
## 📞 Technical Support
|
||||
|
||||
For technical questions about the security architecture:
|
||||
|
||||
- **Security Team:** security@SecureBit.chat
|
||||
- **Technical Documentation:** docs@SecureBit.chat
|
||||
- **GitHub Issues:** [Security Architecture Issues](https://github.com/SecureBitChat/securebit-chat/issues?q=label%3Asecurity-architecture)
|
||||
|
||||
---
|
||||
|
||||
*This document is updated with each major security enhancement. Current version reflects Stage 4 Maximum Security implementation.*
|
||||
|
||||
**Last Updated:** January 14, 2025
|
||||
**Document Version:** 4.0
|
||||
**Security Implementation:** Stage 4 - Maximum Security
|
||||
**Review Status:** ✅ Verified and Tested
|
||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
1
ico.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#fb923c" d="M320 64C324.6 64 329.2 65 333.4 66.9L521.8 146.8C543.8 156.1 560.2 177.8 560.1 204C559.6 303.2 518.8 484.7 346.5 567.2C329.8 575.2 310.4 575.2 293.7 567.2C121.3 484.7 80.6 303.2 80.1 204C80 177.8 96.4 156.1 118.4 146.8L306.7 66.9C310.9 65 315.4 64 320 64zM320 130.8L320 508.9C458 442.1 495.1 294.1 496 205.5L320 130.9L320 130.9z"/></svg>
|
||||
|
After Width: | Height: | Size: 575 B |
3581
index.html
Normal file
14
logo/alby.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 135 61">
|
||||
<path d="M 14.958 15.796 C 14.958 13.48 13.08 11.602 10.764 11.602 C 8.448 11.602 6.57 13.48 6.57 15.796 C 6.57 18.112 8.448 19.99 10.764 19.99 C 13.08 19.99 14.958 18.112 14.958 15.796 Z" fill="rgb(0,0,0)"></path>
|
||||
<path d="M 10.065 15.168 L 17.894 22.996" fill="transparent" stroke-width="2.097" stroke="rgb(0,0,0)" stroke-miterlimit="10" stroke-dasharray=""></path>
|
||||
<path d="M 40.821 15.796 C 40.821 13.48 42.699 11.602 45.015 11.602 C 47.331 11.602 49.209 13.48 49.209 15.796 C 49.209 18.112 47.331 19.99 45.015 19.99 C 42.699 19.99 40.821 18.112 40.821 15.796 Z" fill="rgb(0,0,0)"></path>
|
||||
<path d="M 45.784 15.168 L 37.955 22.996" fill="transparent" stroke-width="2.097" stroke="rgb(0,0,0)" stroke-miterlimit="10" stroke-dasharray=""></path>
|
||||
<path d="M 11.955 40.823 C 9.695 39.747 8.379 37.331 8.82 34.866 C 10.711 24.289 18.593 16.356 28.03 16.356 C 37.489 16.356 45.386 24.327 47.253 34.943 C 47.687 37.412 46.361 39.827 44.093 40.895 C 39.09 43.247 33.628 44.463 28.099 44.455 C 22.514 44.463 16.998 43.222 11.955 40.823 Z" fill="rgb(255,222,110)"></path>
|
||||
<path d="M 9.852 35.05 C 11.679 24.83 19.234 17.404 28.029 17.404 L 28.029 15.307 C 17.953 15.307 9.743 23.747 7.788 34.682 L 9.852 35.051 Z M 28.029 17.404 C 36.846 17.404 44.416 24.867 46.22 35.124 L 48.285 34.761 C 46.355 23.788 38.131 15.307 28.029 15.307 Z M 43.646 39.946 C 38.783 42.233 33.473 43.414 28.099 43.406 L 28.099 45.504 C 33.976 45.504 39.549 44.191 44.539 41.844 Z M 28.1 43.406 C 22.671 43.414 17.309 42.208 12.407 39.876 L 11.505 41.77 C 16.689 44.236 22.359 45.512 28.1 45.504 L 28.1 43.407 Z M 46.221 35.125 C 46.569 37.104 45.509 39.069 43.647 39.945 L 44.54 41.843 C 47.214 40.585 48.807 37.72 48.286 34.761 Z M 7.788 34.68 C 7.26 37.634 8.839 40.5 11.504 41.769 L 12.406 39.875 C 10.55 38.992 9.499 37.025 9.852 35.05 Z" fill="rgb(0,0,0)"></path>
|
||||
<path d="M 15.988 38.193 C 14.168 37.453 13.089 35.517 13.72 33.656 C 15.664 27.918 21.336 23.766 28.03 23.766 C 34.723 23.766 40.395 27.918 42.34 33.656 C 42.97 35.516 41.89 37.452 40.071 38.193 C 36.247 39.748 32.158 40.546 28.03 40.541 C 23.77 40.541 19.704 39.707 15.988 38.193 Z" fill="rgb(0,0,0)"></path>
|
||||
<path d="M 29.357 32.712 C 29.357 31.168 30.922 29.916 32.852 29.916 C 34.782 29.916 36.347 31.168 36.347 32.712 C 36.347 34.256 34.782 35.508 32.852 35.508 C 30.922 35.508 29.357 34.256 29.357 32.712 Z" fill="rgb(248,196,85)"></path>
|
||||
<path d="M 29.357 32.712 C 29.357 31.168 30.922 29.916 32.852 29.916 C 34.782 29.916 36.347 31.168 36.347 32.712 C 36.347 34.256 34.782 35.508 32.852 35.508 C 30.922 35.508 29.357 34.256 29.357 32.712 Z" fill="rgba(255,255,255,0.97)"></path>
|
||||
<path d="M 19.353 32.714 C 19.353 31.17 20.918 29.918 22.848 29.918 C 24.778 29.918 26.343 31.17 26.343 32.714 C 26.343 34.258 24.778 35.51 22.848 35.51 C 20.918 35.51 19.353 34.258 19.353 32.714 Z" fill="rgb(248,196,85)"></path>
|
||||
<path d="M 19.353 32.714 C 19.353 31.17 20.918 29.918 22.848 29.918 C 24.778 29.918 26.343 31.17 26.343 32.714 C 26.343 34.258 24.778 35.51 22.848 35.51 C 20.918 35.51 19.353 34.258 19.353 32.714 Z" fill="rgba(255,255,255,0.97)"></path>
|
||||
<path d="M 72.715 30.751 L 70.2 23.906 L 67.795 30.751 L 72.716 30.751 Z M 66.943 15.84 L 73.677 15.84 L 83.075 40.704 L 76.452 41.37 L 74.639 36.042 L 65.944 36.042 L 64.205 41 L 57.508 41 Z M 91.657 41 L 84.923 41 L 84.923 15.285 L 91.657 14.878 Z M 98.752 41 L 95.348 41 L 95.348 15.285 L 102.082 14.878 L 102.082 37.633 Z M 99.899 30.048 L 98.012 25.719 C 98.456 25.176 99.023 24.659 99.714 24.165 C 100.442 23.64 101.239 23.217 102.082 22.907 C 102.934 22.588 103.836 22.425 104.746 22.426 C 106.522 22.426 108.1 22.759 109.482 23.425 C 110.863 24.091 111.948 25.115 112.738 26.496 C 113.552 27.877 113.959 29.641 113.959 31.787 C 113.959 33.908 113.552 35.684 112.738 37.115 C 111.948 38.545 110.838 39.619 109.408 40.334 C 108.002 41.049 106.349 41.407 104.45 41.407 C 103.71 41.407 102.97 41.296 102.23 41.074 C 101.47 40.831 100.747 40.482 100.084 40.038 C 99.394 39.545 98.702 38.903 98.012 38.114 L 99.899 34.155 C 100.614 34.821 101.317 35.339 102.008 35.709 C 102.698 36.054 103.364 36.227 104.006 36.227 C 104.573 36.227 105.079 36.054 105.523 35.709 C 105.967 35.364 106.324 34.858 106.596 34.192 C 106.867 33.526 107.003 32.737 107.003 31.824 C 107.003 30.911 106.867 30.147 106.596 29.53 C 106.349 28.889 105.979 28.408 105.486 28.087 C 105.017 27.767 104.425 27.606 103.71 27.606 C 103.118 27.606 102.513 27.828 101.897 28.272 C 101.28 28.716 100.614 29.308 99.899 30.048 Z M 120.619 41.074 L 120.767 40.667 L 114.329 24.165 L 120.545 22.389 L 124.208 33.193 L 127.649 23.055 L 134.346 23.055 L 126.465 43.109 C 126.095 44.071 125.491 44.959 124.652 45.773 C 123.787 46.606 122.801 47.304 121.729 47.845 C 120.619 48.412 119.472 48.832 118.288 49.103 L 116.031 43.886 C 116.653 43.626 117.27 43.355 117.881 43.072 C 118.499 42.802 119.093 42.48 119.657 42.11 C 120.175 41.765 120.496 41.42 120.619 41.074 Z" fill="rgb(0,0,0)"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
4
logo/atomic.svg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
10
logo/blink.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="770" height="235" viewBox="0 0 770 235" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M145.76 4.26997C145.66 4.23997 145.55 4.20998 145.44 4.18998C136.01 1.84998 126.57 0.72998 117.27 0.72998H117.23C64.88 0.74998 17.27 36.25 4.09999 89.36C-11.41 151.91 26.72 215.2 89.27 230.71C98.7 233.05 108.15 234.17 117.45 234.17C169.82 234.17 217.45 198.66 230.62 145.54C246.1 83.11 208.15 19.93 145.76 4.26997ZM221.46 143.27C209.57 191.23 166.79 224.73 117.45 224.73C108.77 224.73 100.05 223.66 91.54 221.55C63.73 214.66 40.28 197.35 25.49 172.81C10.7 148.27 6.35999 119.44 13.26 91.64C25.15 43.69 67.97 10.2 117.3 10.17C125.97 10.17 134.67 11.24 143.25 13.37L143.36 13.4L143.47 13.43C200.67 27.78 235.65 86.03 221.46 143.27Z" fill="white"/>
|
||||
<path d="M141.68 101.83C138.33 115.24 117.89 108.36 111.25 106.72L117.11 83.26C123.64 84.87 145.03 87.95 141.57 101.82H141.68V101.83ZM138.03 139.73C134.41 154.34 109.81 146.43 101.84 144.45L108.31 118.55C116.28 120.56 141.82 124.48 138.03 139.73ZM164.84 102.03C166.95 87.75 156.09 80.08 141.38 74.99L146.21 55.65L134.31 52.7L129.62 71.53C126.54 70.76 123.35 70.02 120.2 69.32L124.89 50.29L113.13 47.34L108.3 66.68L100.8 65L84.55 60.94L81.43 73.51C81.43 73.51 89.55 75.49 89.98 75.65C92.61 76.18 95.73 79.01 95.48 82.35L82.27 135.3C81.55 137.56 79.14 138.8 76.88 138.08C76.63 138.01 68.3 135.97 68.3 135.97L62.47 149.38L77.78 153.17L86.16 155.35L81.3 174.89L93.06 177.84L97.89 158.5C101.06 159.37 104.18 160.17 107.24 160.91L102.48 180.31L114.24 183.22L119.1 163.72C139.21 167.51 154.25 166 160.62 147.84C165.75 133.23 160.35 124.82 149.8 119.32C157.5 117.55 163.2 112.63 164.84 102.03Z" fill="white"/>
|
||||
<path d="M338.06 82.01C345.36 82.01 351.67 83.3 356.99 85.89C362.31 88.48 366.72 91.97 370.22 96.38C373.72 100.79 376.3 105.96 377.98 111.89C379.65 117.82 380.49 124.21 380.49 131.05C380.49 141.54 378.55 151.47 374.67 160.82C370.79 170.17 365.51 178.31 358.82 185.23C352.13 192.15 344.22 197.63 335.09 201.65C325.97 205.68 316.08 207.7 305.43 207.7C304.06 207.7 301.67 207.66 298.24 207.59C294.82 207.51 290.9 207.17 286.49 206.56C282.08 205.95 277.4 205.04 272.46 203.82C267.52 202.61 262.84 200.93 258.43 198.8L297.89 32.96L333.25 27.49L319.1 86.35C322.14 84.98 325.18 83.92 328.23 83.16C331.29 82.39 334.56 82.01 338.06 82.01ZM308.41 179.64C313.73 179.64 318.75 178.35 323.47 175.76C328.18 173.18 332.25 169.72 335.67 165.38C339.09 161.05 341.79 156.14 343.77 150.67C345.75 145.19 346.74 139.49 346.74 133.56C346.74 126.26 345.52 120.56 343.09 116.45C340.66 112.35 336.17 110.29 329.63 110.29C327.5 110.29 324.73 110.67 321.3 111.43C317.88 112.19 314.8 113.79 312.06 116.22L297 178.73C297.91 178.88 298.71 179.03 299.39 179.19C300.07 179.34 300.79 179.46 301.56 179.53C302.32 179.61 303.23 179.64 304.3 179.64C305.36 179.64 306.73 179.64 308.41 179.64Z" fill="white"/>
|
||||
<path d="M434.78 206.79C423.98 206.79 415.5 205.38 409.35 202.57C403.19 199.76 398.78 195.92 396.12 191.05C393.46 186.18 392.16 180.6 392.24 174.28C392.32 167.97 393.19 161.24 394.86 154.09L423.83 32.96L459.19 27.49L427.48 158.88C426.87 161.62 426.53 164.13 426.45 166.41C426.37 168.69 426.79 170.71 427.71 172.45C428.62 174.2 430.18 175.61 432.39 176.67C434.59 177.73 437.67 178.42 441.63 178.72L434.78 206.79Z" fill="white"/>
|
||||
<path d="M486.11 204.51H452.35L480.86 84.52H514.85L486.11 204.51Z" fill="white"/>
|
||||
<path d="M539.94 89.99C542.52 89.23 545.38 88.36 548.49 87.37C551.61 86.38 555.11 85.47 558.98 84.63C562.86 83.8 567.19 83.11 571.99 82.58C576.78 82.05 582.22 81.78 588.3 81.78C606.24 81.78 618.56 86.95 625.26 97.29C631.95 107.63 633.09 121.78 628.68 139.72L613.17 204.51H579.18L594.24 141.09C595.15 137.14 595.87 133.3 596.41 129.57C596.94 125.84 596.9 122.57 596.3 119.76C595.69 116.95 594.28 114.67 592.08 112.92C589.87 111.17 586.49 110.3 581.93 110.3C577.52 110.3 573.03 110.76 568.47 111.67L546.34 204.51H512.35L539.94 89.99Z" fill="white"/>
|
||||
<path d="M684.79 127.86C693.91 120.56 702.5 113.11 710.57 105.5C718.63 97.9 725.55 90.9 731.33 84.51H769.65C761.13 93.94 752.27 103.18 743.07 112.23C733.87 121.28 723.34 130.9 711.47 141.09C714.66 145.05 717.89 149.61 721.17 154.78C724.44 159.95 727.56 165.35 730.52 170.98C733.49 176.61 736.22 182.31 738.73 188.09C741.24 193.87 743.33 199.35 745 204.52H707.13C705.76 200.57 704.05 196.23 702 191.52C699.95 186.81 697.67 182.13 695.16 177.49C692.65 172.85 690.03 168.33 687.29 163.92C684.55 159.51 681.82 155.56 679.08 152.06L666.31 204.53H632.55L673.61 32.98L708.97 27.51L684.79 127.86Z" fill="white"/>
|
||||
<path d="M526.82 48.28C526.82 51.44 526.22 54.38 525.01 57.12C523.81 59.86 522.16 62.25 520.09 64.28C518.01 66.31 515.58 67.93 512.8 69.14C510.02 70.34 507.05 70.95 503.9 70.95C500.74 70.95 497.78 70.35 495 69.14C492.22 67.94 489.81 66.32 487.78 64.28C485.75 62.25 484.13 59.86 482.92 57.12C481.72 54.38 481.12 51.44 481.12 48.28C481.12 45.21 481.72 42.28 482.92 39.5C484.12 36.72 485.74 34.31 487.78 32.28C489.82 30.25 492.22 28.63 495 27.42C497.78 26.22 500.75 25.61 503.9 25.61C507.05 25.61 510.02 26.21 512.8 27.42C515.58 28.63 518.01 30.24 520.09 32.28C522.16 34.31 523.8 36.72 525.01 39.5C526.22 42.28 526.82 45.21 526.82 48.28Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
10
logo/breez.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="100" height="38" viewBox="0 0 100 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.8955 24.9263C20.8955 23.7996 19.9893 22.879 18.8639 22.879H6.0827C4.96462 22.879 4.05115 23.7922 4.05115 24.9263C4.05115 26.053 4.95731 26.9735 6.0827 26.9735H18.8639C19.982 26.9662 20.8955 26.053 20.8955 24.9263Z" fill="white"/>
|
||||
<path d="M98.1188 30.05H91.1727C90.9092 30.05 90.7555 29.7602 90.8872 29.5373L98.9825 16.3695C99.0411 16.2803 99.0777 16.1837 99.1216 16.0871C99.1216 16.0797 99.1289 16.0723 99.1289 16.0648C99.1582 15.9905 99.1802 15.9162 99.1948 15.8419C99.2094 15.7825 99.2314 15.723 99.2387 15.6636C99.246 15.6413 99.246 15.6115 99.246 15.5892C99.2607 15.5001 99.2753 15.4109 99.2753 15.3143C99.2753 14.2591 98.4263 13.3971 97.3869 13.3971H87.3958C86.4077 13.3971 85.5294 14.1328 85.4342 15.1285C85.3244 16.2729 86.2101 17.2315 87.3153 17.2315H93.3905C93.654 17.2315 93.8077 17.5213 93.6759 17.7442L85.6319 30.8229C85.5806 30.912 85.544 31.0012 85.5001 31.0904C85.3684 31.3505 85.2878 31.6403 85.2878 31.9524C85.2878 33.0076 86.1296 33.8696 87.1763 33.8696H98.1115C99.1509 33.8696 99.9999 33.0076 99.9999 31.9524C100.007 30.912 99.1582 30.05 98.1188 30.05Z" fill="white"/>
|
||||
<path d="M47.8252 13.2364C47.8178 13.2364 47.8105 13.2364 47.8105 13.229C47.7884 13.229 47.7736 13.2215 47.7516 13.214C47.7368 13.214 47.7148 13.214 47.7 13.2066C47.6853 13.2066 47.6779 13.2066 47.6632 13.2066C47.2142 13.1693 46.1761 13.1693 45.6314 13.2364V13.2439C44.6081 13.3558 43.4891 13.7065 42.7087 14.3035C42.3701 14.5646 42.0609 14.8482 41.7811 15.1541C41.7517 14.102 40.9124 13.2588 39.8671 13.2588C38.807 13.2588 37.953 14.1319 37.953 15.1989V32.13C37.953 32.3986 38.0045 32.6523 38.1076 32.8911C38.4021 33.5925 39.0867 34.085 39.8818 34.085C40.9419 34.085 41.8032 33.212 41.8032 32.1374V32.018V30.1973V22.8996C41.8032 21.1834 42.2597 19.7656 43.1652 18.6911C43.9087 17.8106 44.7332 17.3032 45.6682 17.1539C46.132 17.0942 46.6031 17.1092 46.6031 17.1092H47.5454C48.6055 17.1092 49.4669 16.2361 49.4669 15.1616C49.4595 14.1766 48.7528 13.3782 47.8252 13.2364Z" fill="white"/>
|
||||
<path d="M29.2997 16.5302C30.2408 15.2095 30.7995 13.5904 30.7995 11.8294C30.7995 7.38225 27.2486 3.77827 22.867 3.77827H21.7789V2.90526C21.7789 1.79347 20.8894 0.898071 19.8013 0.898071C18.7059 0.898071 17.8237 1.80093 17.8237 2.90526V3.77827H15.8094V3.78573H14.5375C13.4789 3.78573 12.6261 4.65875 12.6261 5.72576C12.6261 6.80024 13.4862 7.6658 14.5375 7.6658H15.8094V7.67326H22.8376C25.0357 7.67326 26.8149 9.47898 26.8149 11.71V11.7175C26.8149 13.8888 25.124 15.702 22.9993 15.7468C22.6097 15.7542 21.4334 15.7468 21.4334 15.7468C21.4261 15.7468 21.4114 15.7468 21.404 15.7468H21.3893C20.2204 15.7766 19.2867 16.7989 19.4117 18.0226C19.522 19.0896 20.463 19.8731 21.5216 19.8731H25.5357C25.9474 19.8731 26.3664 19.9029 26.7634 20.0074C28.9174 20.5819 30.5128 22.5667 30.5128 24.9395C30.5128 27.7526 28.2631 30.0358 25.4916 30.0358H13.3759H11.0969C10.0236 30.0358 9.06784 30.8342 8.96491 31.9162C8.84729 33.1548 9.81036 34.1995 11.0087 34.1995H17.8237V35.0949C17.8237 36.2066 18.7133 37.102 19.8013 37.102C20.8967 37.102 21.7789 36.1992 21.7789 35.0949V34.1995H23.4992C23.4992 34.1995 23.4992 34.1995 23.5066 34.1995H25.668C30.6892 34.1995 34.7547 30.0657 34.7547 24.9768C34.7621 21.2012 32.5124 17.948 29.2997 16.5302Z" fill="white"/>
|
||||
<path d="M13.2196 17.8148C13.2196 16.6881 12.3084 15.7676 11.1768 15.7676H2.04283C0.918539 15.7676 0 16.6807 0 17.8148C0 18.9415 0.91119 19.8621 2.04283 19.8621H11.1768C12.3084 19.8621 13.2196 18.9415 13.2196 17.8148Z" fill="white"/>
|
||||
<path d="M9.29228 13.828H18.4262C19.5505 13.828 20.4691 12.9149 20.4691 11.7808C20.4691 10.654 19.5579 9.73352 18.4262 9.73352H9.29228C8.16799 9.73352 7.24945 10.6467 7.24945 11.7808C7.24945 12.9075 8.16064 13.828 9.29228 13.828Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M63.6184 15.4661C64.9246 16.7832 65.67 18.398 65.8471 20.2882V20.303C65.9357 21.1662 65.8397 22.7364 65.8397 22.7364C65.8397 24.0089 64.8139 25.0433 63.552 25.0433H54.4967C54.3122 25.0433 54.1646 25.1921 54.1646 25.3781V25.9363C54.1646 27.3799 54.6148 28.5036 55.5521 29.3593C56.4893 30.2151 57.7218 30.6467 59.2199 30.6467C59.6184 30.6467 60.0465 30.6021 60.4376 30.5277C61.1387 30.3639 61.9431 29.9993 62.504 29.5677C62.5372 29.5454 62.5686 29.5231 62.5999 29.5007C62.6313 29.4784 62.6627 29.4561 62.6959 29.4338C62.7033 29.43 62.7106 29.4245 62.718 29.4189C62.7254 29.4133 62.7328 29.4077 62.7402 29.404C62.8066 29.3593 62.8804 29.3147 62.9468 29.2775C62.9505 29.2738 62.956 29.2719 62.9616 29.27C62.9671 29.2682 62.9726 29.2663 62.9763 29.2626L63.1756 29.1733C63.1903 29.1733 63.2051 29.1659 63.2198 29.1584L63.2199 29.1584C63.2937 29.1287 63.3675 29.1063 63.4486 29.0914C63.456 29.0914 63.4652 29.0896 63.4745 29.0877C63.4837 29.0859 63.4929 29.084 63.5003 29.084C63.5741 29.0691 63.6553 29.0617 63.7365 29.0617H63.7586H63.766C64.5409 29.0617 65.1977 29.5677 65.4338 30.2746C65.4338 30.2895 65.4412 30.3044 65.4486 30.3193C65.456 30.3565 65.4634 30.3863 65.4707 30.416C65.4855 30.4532 65.4929 30.483 65.5003 30.5202C65.5224 30.617 65.5298 30.7211 65.5298 30.8179V30.8476V30.8849C65.5298 30.9518 65.5224 31.0262 65.515 31.0932C65.5076 31.1081 65.5076 31.1155 65.5076 31.1304C65.4929 31.2123 65.4707 31.2941 65.4486 31.3685C65.4449 31.3797 65.4412 31.389 65.4375 31.3983C65.4339 31.4076 65.4302 31.4169 65.4265 31.4281C65.4117 31.4876 65.3896 31.5471 65.3601 31.5992C65.3578 31.6037 65.3549 31.6089 65.3517 31.6146C65.3444 31.6276 65.3357 31.6432 65.3305 31.6588C65.2936 31.7332 65.2493 31.8002 65.2051 31.8671C64.9173 32.3285 64.3859 32.775 63.5667 33.2512C62.3712 33.9508 60.873 34.3005 59.1166 34.3005C56.5483 34.3005 54.4229 33.5117 52.7919 31.9639C51.1536 30.416 50.3196 28.3473 50.3196 25.8321V21.4416C50.3196 19.2091 51.1019 17.2595 52.6443 15.6372C54.1868 14.0075 56.0686 13.1815 58.2384 13.1815C60.2974 13.1815 62.1129 13.948 63.6184 15.4661ZM61.4487 21.7467C61.8546 21.7467 62.1867 21.4193 62.1867 21.01V20.7495C62.1867 19.5663 61.8029 18.7924 61.0207 17.8994C60.2458 17.0213 59.3233 16.5972 58.2015 16.5972C57.0945 16.5972 56.172 16.9916 55.3676 17.8101C54.5632 18.6287 54.172 19.611 54.172 20.8091V20.9951C54.172 21.4044 54.5041 21.7393 54.91 21.7393L61.4487 21.7467Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M81.5291 15.4661C82.8353 16.7832 83.5807 18.398 83.7578 20.2882V20.303C83.8464 21.1662 83.7505 22.7364 83.7505 22.7364C83.7505 24.0089 82.7246 25.0433 81.4627 25.0433H72.4074C72.2229 25.0433 72.0753 25.1921 72.0753 25.3781V25.9363C72.0753 27.3799 72.5255 28.5036 73.4628 29.3593C74.4 30.2151 75.6325 30.6467 77.1306 30.6467C77.5291 30.6467 77.9572 30.6021 78.3483 30.5277C79.0494 30.3639 79.8538 29.9993 80.4147 29.5677C80.4479 29.5454 80.4793 29.5231 80.5106 29.5007C80.542 29.4784 80.5734 29.4561 80.6066 29.4338C80.614 29.43 80.6213 29.4245 80.6287 29.4189C80.6361 29.4133 80.6435 29.4077 80.6509 29.404C80.7173 29.3593 80.7911 29.3147 80.8575 29.2775C80.8612 29.2738 80.8667 29.2719 80.8723 29.27C80.8778 29.2682 80.8833 29.2663 80.887 29.2626L81.0863 29.1733C81.101 29.1733 81.1158 29.1659 81.1305 29.1584L81.1306 29.1584C81.2044 29.1287 81.2782 29.1063 81.3593 29.0914C81.3667 29.0914 81.3759 29.0896 81.3852 29.0877C81.3944 29.0859 81.4036 29.084 81.411 29.084C81.4848 29.0691 81.566 29.0617 81.6472 29.0617H81.6693H81.6767C82.4516 29.0617 83.1084 29.5677 83.3446 30.2746C83.3446 30.2895 83.3519 30.3044 83.3593 30.3193C83.3667 30.3565 83.3741 30.3863 83.3814 30.416C83.3962 30.4532 83.4036 30.483 83.411 30.5202C83.4331 30.617 83.4405 30.7211 83.4405 30.8179V30.8476V30.8849C83.4405 30.9518 83.4331 31.0262 83.4257 31.0932C83.4184 31.1081 83.4184 31.1155 83.4184 31.1304C83.4036 31.2123 83.3815 31.2941 83.3593 31.3685C83.3556 31.3797 83.3519 31.389 83.3482 31.3983C83.3446 31.4076 83.3409 31.4169 83.3372 31.4281C83.3224 31.4876 83.3003 31.5471 83.2708 31.5992C83.2685 31.6037 83.2656 31.6089 83.2624 31.6146C83.2551 31.6276 83.2464 31.6432 83.2412 31.6588C83.2043 31.7332 83.1601 31.8002 83.1158 31.8671C82.828 32.3285 82.2966 32.775 81.4774 33.2512C80.2819 33.9508 78.7837 34.3005 77.0273 34.3005C74.4591 34.3005 72.3336 33.5117 70.7026 31.9639C69.0643 30.416 68.2303 28.3473 68.2303 25.8321V21.4416C68.2303 19.2091 69.0126 17.2595 70.555 15.6372C72.0975 14.0075 73.9794 13.1815 76.1491 13.1815C78.2081 13.1815 80.0236 13.948 81.5291 15.4661ZM79.3594 21.7467C79.7653 21.7467 80.0974 21.4193 80.0974 21.01V20.7495C80.0974 19.5663 79.7136 18.7924 78.9314 17.8994C78.1565 17.0213 77.234 16.5972 76.1122 16.5972C75.0052 16.5972 74.0827 16.9916 73.2783 17.8101C72.4739 18.6287 72.0827 19.611 72.0827 20.8091V20.9951C72.0827 21.4044 72.4148 21.7393 72.8207 21.7393L79.3594 21.7467Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
BIN
logo/favicon.ico
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
13
logo/impervious.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="393" height="218" viewBox="0 0 393 218" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M185.301 76.795V73.897L185.307 73.905L225.402 124.032V124.031L225.403 124.028L229.231 114.193C236.413 106.691 260.404 80.316 261.55 61.58C258.814 62.315 256.207 62.859 253.718 63.258L245.862 51.292C244.101 39.739 240.132 29.536 233.454 19.353L231.067 15.714L227.99 18.793C227.591 19.193 227.209 19.591 226.823 19.99C226.755 19.986 226.705 19.989 226.663 19.995C221.585 12.082 210.903 6.614 198.508 6.614C198.498 6.614 198.489 6.615 198.479 6.615L196.859 0C196.859 0 196.847 0.000999451 196.846 0.000999451C196.596 0.0139995 162.857 1.87601 150.064 31.387C149.217 33.309 148.486 35.369 147.849 37.532C147.809 37.669 147.763 37.799 147.723 37.937C147.497 38.723 147.302 39.547 147.104 40.366C147.017 40.727 146.913 41.068 146.831 41.436C146.784 41.647 146.755 41.849 146.71 42.059C146.633 42.417 146.574 42.767 146.504 43.122C142.038 65.53 148.815 82.503 156.603 93.788C156.569 98.216 155.501 100.957 154.722 102.324C154.547 102.603 154.362 102.882 154.163 103.162H149.912L141.285 120.572H138.312L130.5 136.338C130.497 136.344 130.494 136.35 130.491 136.356C140.319 130.158 151.95 126.558 164.428 126.558C174.161 126.558 183.379 128.748 191.63 132.649C201.593 137.359 210.128 144.579 216.443 153.489C217.588 155.105 218.659 156.776 219.653 158.498H225.525H236.994L185.301 76.795ZM212.681 98.214L209.294 93.98L209.509 93.705L210.488 92.45L223.811 75.359C225.669 75.665 227.693 75.955 229.837 76.207L213.221 97.522L212.681 98.214ZM218.305 105.245L214.918 101.011L215.673 100.042L217.303 97.951L233.934 76.619C236.027 76.79 238.189 76.899 240.373 76.937L219.385 103.858L218.305 105.245ZM252.017 76.251L225.553 110.198L223.931 112.278L220.544 108.044L221.84 106.381L224.002 103.608L244.825 76.897C247.251 76.81 249.667 76.608 252.017 76.251ZM220.464 51.026L220.757 47.064L222.54 48.493C223.754 49.5 226.301 51.58 229.957 53.594H229.958C232.469 54.977 235.5 56.319 239.035 57.264L240.371 59.298L242.512 62.559L243.543 64.129C243.334 64.13 243.129 64.127 242.923 64.127C228.073 64.075 219.902 58.659 219.902 58.659L220.464 51.026ZM214.213 73.424C215.36 73.705 217.277 74.147 219.751 74.63L219.881 74.731L207.481 90.636L207.056 91.181L203.669 86.947L214.213 73.424Z" fill="white"/>
|
||||
<path d="M8.687 183.487H0V217.101H8.687V183.487Z" fill="white"/>
|
||||
<path d="M42.802 209.249H42.606L32.153 183.487H19.689V217.101H27.247V192.711H27.442L37.502 217.101H46.924L56.935 192.711H57.131V217.101H65.818V183.487H53.401L42.802 209.249Z" fill="white"/>
|
||||
<path d="M108.394 186.429C105.155 183.78 100.297 183.486 93.7701 183.486H76.8401V217.1H85.5271V209.613V207.628V201.201V194.008V189.913H93.5241C99.4141 189.913 103.094 190.355 103.094 195.459C103.094 199.974 100.445 201.201 93.6221 201.201H90.5741V207.628H91.8081C100.051 207.628 104.371 207.286 108.001 204.587C110.7 202.673 111.927 199.58 111.927 195.361C111.927 191.386 110.798 188.393 108.394 186.429Z" fill="white"/>
|
||||
<path d="M129.372 203.164H147.529V196.687H129.372V190.159H149.001V183.487H120.685V217.101H149.834V210.378H129.372V203.164Z" fill="white"/>
|
||||
<path d="M189.8 202.722C192.449 200.906 193.676 198.307 193.676 194.331C193.676 190.601 192.547 188 190.094 186.186C186.855 183.781 182.097 183.487 175.52 183.487H159.179V217.101H167.866V209.614V205.569V199.436V194.009V189.914H175.57C181.312 189.914 184.844 190.21 184.844 194.577C184.844 198.601 182.195 199.436 175.668 199.436H172.913V205.569C172.913 205.569 175.03 205.569 175.816 205.519L184.796 217.101H195.297L185.286 204.538C187.053 204.195 188.573 203.605 189.8 202.722Z" fill="white"/>
|
||||
<path d="M220.103 209.151H219.858L207.835 183.487H198.069L214.901 217.101H224.619L241.401 183.487H232.077L220.103 209.151Z" fill="white"/>
|
||||
<path d="M256.245 183.487H247.558V217.101H256.245V183.487Z" fill="white"/>
|
||||
<path d="M302.482 186.186C298.408 183.535 293.208 182.799 286.483 182.799C279.86 182.799 274.607 183.486 270.536 186.186C265.923 189.227 264.353 194.332 264.353 200.269C264.353 206.257 265.923 211.261 270.536 214.304C274.075 216.692 278.482 217.552 283.985 217.738V210.908C276.578 210.325 273.235 206.984 273.235 200.023C273.235 192.123 277.405 189.57 286.533 189.57C295.709 189.57 299.783 192.811 299.783 200.515C299.783 207.627 296.341 210.432 289.032 210.922V217.739C294.489 217.555 298.937 216.701 302.481 214.352C307.044 211.261 308.665 206.257 308.665 200.269C308.665 194.331 307.045 189.276 302.482 186.186Z" fill="white"/>
|
||||
<path d="M344.711 202.035C344.711 204.832 344.319 206.992 342.847 208.514C341.179 210.378 338.282 211.015 334.798 211.015C331.364 211.015 328.419 210.28 326.751 208.464C325.23 206.796 324.789 204.538 324.789 201.691V183.487H316.102V202.331C316.102 207.827 316.789 211.655 320.126 214.254C323.954 217.249 329.009 217.788 334.7 217.788C340.344 217.788 345.301 217.249 349.177 214.254C352.563 211.655 353.251 207.827 353.251 202.331V183.487H344.712V202.035H344.711Z" fill="white"/>
|
||||
<path d="M379.038 196.295C376.633 195.901 373.933 195.558 372.069 195.066C370.449 194.674 369.859 193.889 369.859 192.467C369.859 191.14 370.35 190.307 371.971 189.914C373.491 189.62 375.503 189.522 377.269 189.522C381.147 189.522 386.004 189.866 389.931 190.307V183.338C385.562 183.044 381.49 182.799 377.416 182.799C373.884 182.799 369.858 183.045 366.915 184.467C363.037 186.331 361.615 189.522 361.615 193.447C361.615 199.776 365.393 202.132 374.03 203.556C376.581 203.998 379.428 204.389 381.342 204.881C383.01 205.372 383.599 206.158 383.599 207.678C383.599 209.1 383.205 210.033 381.439 210.427C379.966 210.721 377.415 210.916 374.47 210.916C370.888 210.916 365.735 210.524 361.712 210.083V217.247C366.619 217.541 371.036 217.786 375.599 217.786C379.379 217.786 383.696 217.392 386.689 215.92C390.567 214.008 392.039 210.767 392.039 206.696C392.043 199.877 387.675 197.62 379.038 196.295Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
37
logo/lightning-labs.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="192px" height="55px" viewBox="0 0 192 55" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 62 (91390) - https://sketch.com -->
|
||||
<title>logo-invert</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Delivery" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Careers" transform="translate(-69.000000, -38.000000)" fill="#F5F5F5" fill-rule="nonzero">
|
||||
<g id="logo-invert" transform="translate(69.000000, 38.461884)">
|
||||
<g id="Group" transform="translate(40.604167, 1.000000)">
|
||||
<g id="Path">
|
||||
<path d="M12.8285417,19.02875 L2.54166667,19.02875 L2.54166667,0.862083333 C2.40541667,0.407916667 2.291875,0.294375 1.86041667,0.3625 L0.838541667,0.49875 C0.270833333,0.589583333 0.270833333,0.77125 0.270833333,1.27083333 L0.270833333,19.165 C0.146500018,19.678633 0.274513564,20.2207685 0.615470495,20.6245332 C0.956427427,21.028298 1.46946502,21.2453021 1.99666667,21.20875 L12.57875,21.20875 C13.191875,21.20875 13.464375,20.9135417 13.600625,20.323125 L13.736875,19.8008333 C13.8958333,19.2104167 13.6233333,19.0060417 12.8285417,19.02875 Z"></path>
|
||||
<path d="M18.4375,0.0445833333 L17.3702083,0.203541667 C16.93875,0.203541667 16.8025,0.453333333 16.8025,0.952916667 L16.8025,21.0952083 C16.8025,21.549375 16.93875,21.685625 17.3702083,21.6175 L18.4375,21.4585417 C18.8689583,21.4585417 18.9825,21.2314583 18.9825,20.731875 L18.9825,0.566875 C19.050625,0.112708333 18.9370833,-0.000833333333 18.4375,0.0445833333 Z"></path>
|
||||
<path d="M38.875,10.85375 L33.1297917,10.85375 C32.5847917,10.85375 32.3804167,11.0354167 32.3804167,11.67125 L32.3804167,11.9664583 C32.3804167,12.556875 32.5847917,12.76125 33.1297917,12.76125 L37.6714583,12.76125 L37.6714583,18.5745833 C36.4115259,18.8655912 35.1261947,19.0329124 33.83375,19.0741667 C28.2020833,19.0741667 25.25,16.2810417 25.25,10.85375 C25.25,5.42645833 28.179375,2.63333333 33.6747917,2.63333333 C35.1005763,2.66891863 36.5176504,2.86700427 37.8985417,3.22375 C38.670625,3.428125 38.9658333,3.36 39.1247917,2.83770833 L39.1247917,2.38354167 C39.28375,1.77041667 39.1247917,1.49791667 38.2391667,1.29354167 C36.5922953,0.864377635 34.8996282,0.635638831 33.1979167,0.612291667 C26.7033333,0.612291667 23.07,4.31375 23.07,10.85375 C23.07,17.39375 26.7260417,21.0952716 33.1979167,21.0952716 C34.7882733,21.0991773 36.3735876,20.9162564 37.92125,20.5502083 C39.1702083,20.3004167 39.624375,19.8008333 39.624375,18.8470833 L39.624375,11.8529167 C39.7379167,11.12625 39.488125,10.85375 38.875,10.85375 Z"></path>
|
||||
<path d="M58.971875,0.385208333 L57.9272917,0.521458333 C57.54125,0.521458333 57.405,0.77125 57.405,1.248125 L57.405,9.24145833 L46.2552083,9.24145833 L46.2552083,0.884791667 C46.2552083,0.430625 46.1189583,0.317083333 45.6875,0.385208333 L44.6429167,0.521458333 C44.2341667,0.521458333 44.120625,0.77125 44.0752083,1.248125 L44.0752083,20.8 C44.0752083,21.2314583 44.2341667,21.345 44.6429167,21.2995833 L45.6875,21.140625 C46.1189583,21.140625 46.2325,20.9135417 46.2552083,20.4366667 L46.2552083,11.3533333 L57.405,11.3533333 L57.405,20.8 C57.405,21.2314583 57.54125,21.345 57.9727083,21.2995833 L58.9945833,21.140625 C59.380625,21.140625 59.516875,20.9135417 59.585,20.4366667 L59.585,0.884791667 C59.516875,0.430625 59.3125,0.317083333 58.971875,0.385208333 Z"></path>
|
||||
<path d="M80.0679167,1.86125 L80.0679167,1.36166667 C79.75,0.77125 79.5229167,0.589583333 78.9097917,0.521458333 L64.2402083,0.521458333 C63.604375,0.521458333 63.331875,0.77125 63.195625,1.36166667 L63.0820833,1.86125 C62.923125,2.45166667 63.195625,2.65604167 63.9904167,2.70145833 L70.4622917,2.70145833 L70.4622917,20.868125 C70.4622917,21.2995833 70.6439583,21.413125 71.0754167,21.3677083 L72.0972917,21.20875 C72.4833333,21.20875 72.6422917,20.9816667 72.6422917,20.5047917 L72.6422917,2.70145833 L79.1595833,2.70145833 C79.9770833,2.65604167 80.226875,2.45166667 80.0679167,1.86125 Z"></path>
|
||||
<path d="M100.936875,0.884791667 L100.936875,20.8908333 C100.936875,21.2541667 100.936875,21.3904167 100.936875,21.3904167 C100.936875,21.3904167 100.687083,21.5039583 100.391875,21.1860417 L85.6995833,4.88145833 L85.6995833,20.4139583 C85.6995833,20.9135417 85.6995833,21.0952083 85.1545833,21.1633333 L84.0872917,21.2995833 C83.6558333,21.2995833 83.5195787,21.2995833 83.5195787,20.8 L83.5195787,1.13458333 C83.5181392,0.695901963 83.8544271,0.329941575 84.2916667,0.294375 C84.5634009,0.245856781 84.8419474,0.330264808 85.0410417,0.521458333 C85.0410417,0.521458333 98.8704167,16.0085417 98.8704167,16.0085417 L98.8704167,1.27083333 C98.8704167,0.793958333 99.0066667,0.612291667 99.438125,0.544166667 L100.391875,0.3625 C100.823333,0.294375 100.936875,0.430625 100.936875,0.884791667 Z"></path>
|
||||
<path d="M107.454167,0.0445833333 L106.386875,0.203541667 C105.955417,0.203541667 105.841875,0.430625 105.841875,0.930208333 L105.841875,21.1179167 C105.841875,21.5720833 105.955417,21.685625 106.386875,21.6402083 L107.454167,21.48125 C107.885625,21.48125 108.021875,21.2314583 108.021875,20.7545833 L108.021875,0.566875 C108.021875,0.112708333 107.885625,-0.0235416667 107.454167,0.0445833333 Z"></path>
|
||||
<path d="M150.145833,10.85375 L144.128125,10.85375 C143.583125,10.85375 143.37875,11.0354167 143.37875,11.67125 L143.37875,11.9664583 C143.37875,12.556875 143.583125,12.76125 144.128125,12.76125 L148.82875,12.76125 L148.82875,18.5745833 C147.568539,18.8640434 146.283359,19.0313449 144.991042,19.0741667 C139.495625,19.0741667 136.520833,16.2810417 136.520833,10.85375 C136.520833,5.42645833 139.450208,2.63333333 144.945625,2.63333333 C146.36404,2.66765468 147.773738,2.86577437 149.146667,3.22375 C149.941458,3.428125 150.236667,3.36 150.395625,2.83770833 L150.395625,2.38354167 C150.554583,1.77041667 150.395625,1.49791667 149.487292,1.29354167 C147.84813,0.864442921 146.162984,0.635690061 144.46875,0.612291667 C137.974167,0.612291667 134.363542,4.31375 134.363542,10.85375 C134.363542,17.39375 138.019583,21.0952382 144.46875,21.0952382 C146.059036,21.097927 147.644205,20.9150229 149.192083,20.5502083 C150.418333,20.3004167 150.8725,19.8008333 150.8725,18.8470833 L150.8725,11.8529167 C150.986042,11.12625 150.758958,10.85375 150.145833,10.85375 Z"></path>
|
||||
<path d="M130.593958,0.884791667 L130.593958,20.8908333 C130.593958,21.2541667 130.593958,21.3904167 130.593958,21.3904167 C130.593958,21.3904167 130.344167,21.5039583 130.048958,21.1860417 L115.356667,4.88145833 L115.356667,20.4139583 C115.356667,20.9135417 115.243125,21.0952083 114.811667,21.1633333 L113.8125,21.2995833 C113.40375,21.2995833 113.2675,21.2995833 113.2675,20.8 L113.2675,1.13458333 C113.244166,0.765016116 113.465531,0.423745601 113.8125,0.294375 C114.077338,0.245779277 114.349123,0.330711863 114.539167,0.521458333 L128.39125,16.0085417 L128.39125,1.27083333 C128.39125,0.793958333 128.5275,0.612291667 128.93625,0.544166667 L130.048958,0.3625 C130.480417,0.294375 130.593958,0.430625 130.593958,0.884791667 Z"></path>
|
||||
</g>
|
||||
<g transform="translate(1.270833, 30.791667)">
|
||||
<path d="M12.625,19.2339583 L2.27,19.2339583 L2.27,1.18083333 C2.27,0.749375 2.065625,0.613125 1.656875,0.68125 L0.612291667,0.8175 C0.203541667,0.8175 0.0672916667,1.06729167 0.112708333,1.54416667 L0.112708333,19.4610417 C0.112708333,20.8008333 0.635,21.39125 1.88395833,21.4139583 L12.4660417,21.4139583 C13.101875,21.4139583 13.3516667,21.2095833 13.510625,20.6191667 L13.6241667,20.1195833 C13.7377083,19.5064583 13.510625,19.3020833 12.625,19.2339583 Z" id="Path"></path>
|
||||
<path d="M33.4258333,20.7554167 L25.091875,1.294375 C24.9447855,0.861600921 24.5384414,0.570535145 24.0813542,0.570535145 C23.6242669,0.570535145 23.2179229,0.861600921 23.0708333,1.294375 L14.736875,20.7554167 C14.5552083,21.2095833 14.5779167,21.323125 14.8958333,21.39125 L16.03125,21.39125 C16.2002858,21.4466209 16.3850364,21.4258544 16.5375618,21.3343391 C16.6900872,21.2428239 16.795352,21.0895811 16.8260417,20.914375 L19.096875,15.873125 L29.11125,15.873125 L31.3820833,20.914375 C31.412773,21.0895811 31.5180378,21.2428239 31.6705632,21.3343391 C31.8230886,21.4258544 32.0078392,21.4466209 32.176875,21.39125 L33.3122917,21.39125 C33.5847917,21.323125 33.6529167,21.2095833 33.4258333,20.7554167 Z M19.8008333,14.3516667 L24.001875,3.79229167 L28.1802083,14.3516667 L19.8008333,14.3516667 Z" id="Shape"></path>
|
||||
<path d="M46.8010417,10.6047917 C48.6435779,9.6555896 49.742476,7.69901489 49.5941667,5.63166667 C49.7070909,4.39659434 49.3100185,3.16906571 48.4951486,2.23410976 C47.6802788,1.2991538 46.5185032,0.73810954 45.2795833,0.68125 L37.4679167,0.68125 C36.400625,0.68125 36.0145833,1.27166667 36.0145833,2.4525 L36.0145833,19.7335417 C36.0145833,20.8689583 36.400625,21.323125 37.4679167,21.4139583 L45.1433333,21.4139583 C48.0855205,21.2796562 50.3937556,18.8409557 50.36625,15.8958333 C50.5023032,13.5312576 49.0436841,11.3665553 46.8010417,10.6047917 Z M38.1945833,2.656875 L44.848125,2.656875 C46.5739583,2.656875 47.6185417,4.58708333 47.6185417,5.90416667 C47.6185417,8.175 46.1652083,9.71916667 43.2585417,9.71916667 L38.1945833,9.71916667 L38.1945833,2.656875 Z M45.0070833,19.2339583 L38.1945833,19.2339583 L38.1945833,11.3541667 L42.4864583,11.3541667 C46.483125,11.3541667 48.4360417,12.67125 48.4360417,15.39625 C48.5320513,16.3772469 48.2096076,17.3532083 47.5480727,18.0839233 C46.8865378,18.8146383 45.9473491,19.2322448 44.9616667,19.2339583 L45.0070833,19.2339583 Z" id="Shape"></path>
|
||||
<path d="M54.8852083,6.35833333 C54.8852083,4.0875 56.6110417,2.67958333 59.426875,2.67958333 C60.7240708,2.6895312 62.0115715,2.90411465 63.241875,3.31541667 C63.923125,3.56520833 64.195625,3.49708333 64.3772917,3.11104167 L64.5816667,2.49791667 C64.7860417,1.9075 64.5816667,1.635 63.8322917,1.3625 C62.4435706,0.850640444 60.975049,0.589228222 59.495,0.590412633 C55.1804167,0.590412633 52.6825,2.86125 52.6825,6.63083333 C52.6825,12.7620833 64.2183333,11.85375 64.2183333,16.0775 C64.2183333,18.3483333 62.424375,19.5972917 59.6766667,19.5972917 C57.8467238,19.6247206 56.0305636,19.2769452 54.3402083,18.5754167 C53.5908333,18.3029167 53.2502083,18.3483333 53.0458333,18.8025 L52.81875,19.4383333 C52.6370833,19.8925 52.81875,20.165 53.295625,20.369375 C55.2246045,21.208249 57.3008979,21.6559256 59.4041667,21.6864583 C63.9458333,21.6864583 66.3529503,19.5972917 66.3529503,15.7822917 C66.375625,9.81 54.8852083,10.5139583 54.8852083,6.35833333 Z" id="Path"></path>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group">
|
||||
<path d="M27.25,30.2475 L11.3541667,53.4327083 C11.0952102,53.812418 10.579216,53.9135933 10.1960417,53.6597917 C10.0071268,53.5351125 9.87650739,53.3394851 9.83376203,53.1172092 C9.79101668,52.8949334 9.83976152,52.6648129 9.96895833,52.4789583 L25.8647917,29.29375 C26.1281625,28.9112353 26.6517561,28.81465 27.0342708,29.0780208 C27.4167856,29.3413916 27.5133708,29.8649853 27.25,30.2475 L27.25,30.2475 Z" id="Path"></path>
|
||||
<path d="M25.1608333,26.1145833 L9.62833333,48.8229167 C9.36496254,49.2054314 8.84136889,49.3020166 8.45885417,49.0386458 C8.07633945,48.775275 7.97975421,48.2516814 8.243125,47.8691667 L23.775625,25.1608333 C24.0389958,24.7783186 24.5625894,24.6817334 24.9451042,24.9451042 C25.3276189,25.208475 25.4242041,25.7320686 25.1608333,26.1145833 L25.1608333,26.1145833 Z" id="Path"></path>
|
||||
<path d="M20.4375,1.31708333 L2.42979167,27.6360417 L19.3247917,27.6360417 L18.189375,29.3164583 L0.522291667,29.3164583 L0.363333333,29.3164583 C-0.0163763088,29.0575018 -0.117551629,28.5415077 0.13625,28.1583333 L19.006875,0.363333333 C19.2658315,-0.0163763088 19.7818256,-0.117551629 20.165,0.13625 C20.5634412,0.389129753 20.6848369,0.91517786 20.4375,1.31708333 Z" id="Path"></path>
|
||||
<path d="M22.1179167,6.35833333 L10.2414583,23.7302083 L22.004375,23.7302083 L20.84625,25.410625 L7.06229167,25.410625 L8.19770833,23.7302083 L20.7327083,5.42729167 C21.0099982,5.1035648 21.4857595,5.03876639 21.8395259,5.27654379 C22.1932923,5.51432119 22.3129575,5.97932434 22.1179167,6.35833333 L22.1179167,6.35833333 Z" id="Path"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
13
logo/lnbits.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="496px" height="148px" viewBox="0 0 496 148" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Group 6</title>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Desktop-HD" transform="translate(-68.000000, -665.000000)" fill-rule="nonzero">
|
||||
<g id="Group-9" transform="translate(25.405063, 654.000000)">
|
||||
<g id="Group-6" transform="translate(43.000000, 11.000000)">
|
||||
<path d="M168.594937,119 L168.594937,99.05 L117.294937,99.05 L117.294937,15.95 L95.2449367,15.95 L95.2449367,119 L168.594937,119 Z M200.094937,119 L200.094937,52.4 L249.894937,119 L271.944937,119 L271.944937,15.95 L249.894937,15.95 L249.894937,82.55 L200.094937,15.95 L178.044937,15.95 L178.044937,119 L200.094937,119 Z M321.494937,120.8 C328.194937,120.8 334.269937,119.075 339.719937,115.625 C345.169937,112.175 349.444937,107.35 352.544937,101.15 C355.644937,94.95 357.194937,87.95 357.194937,80.15 C357.194937,72.35 355.644937,65.375 352.544937,59.225 C349.444937,53.075 345.169937,48.275 339.719937,44.825 C334.269937,41.375 328.194937,39.65 321.494937,39.65 C315.394937,39.65 309.919937,41.075 305.069937,43.925 C300.219937,46.775 296.294937,50.8 293.294937,56 L293.294937,56 L293.294937,14.15 L282.044937,14.15 L282.044937,119 L293.294937,119 L293.294937,104.45 C296.294937,109.65 300.219937,113.675 305.069937,116.525 C309.919937,119.375 315.394937,120.8 321.494937,120.8 Z M319.394937,110.75 C314.394937,110.75 309.894937,109.45 305.894937,106.85 C301.894937,104.25 298.794937,100.625 296.594937,95.975 C294.394937,91.325 293.294937,86.05 293.294937,80.15 C293.294937,74.25 294.394937,69 296.594937,64.4 C298.794937,59.8 301.894937,56.2 305.894937,53.6 C309.894937,51 314.394937,49.7 319.394937,49.7 C324.394937,49.7 328.869937,51 332.819937,53.6 C336.769937,56.2 339.844937,59.8 342.044937,64.4 C344.244937,69 345.344937,74.25 345.344937,80.15 C345.344937,86.05 344.244937,91.325 342.044937,95.975 C339.844937,100.625 336.769937,104.25 332.819937,106.85 C328.869937,109.45 324.394937,110.75 319.394937,110.75 Z M368.844937,27.35 C370.844937,27.35 372.569937,26.6 374.019937,25.1 C375.469937,23.6 376.194937,21.85 376.194937,19.85 C376.194937,17.85 375.469937,16.125 374.019937,14.675 C372.569937,13.225 370.844937,12.5 368.844937,12.5 C366.744937,12.5 364.969937,13.225 363.519937,14.675 C362.069937,16.125 361.344937,17.85 361.344937,19.85 C361.344937,21.85 362.069937,23.6 363.519937,25.1 C364.969937,26.6 366.744937,27.35 368.844937,27.35 Z M374.394937,119 L374.394937,41.45 L363.144937,41.45 L363.144937,119 L374.394937,119 Z M415.494937,120.8 C420.894937,120.8 425.694937,119.15 429.894937,115.85 L429.894937,115.85 L424.344937,107.6 C423.444937,108.5 422.294937,109.25 420.894937,109.85 C419.494937,110.45 417.944937,110.75 416.244937,110.75 C413.944937,110.75 411.969937,109.825 410.319937,107.975 C408.669937,106.125 407.844937,103.8 407.844937,101 L407.844937,101 L407.844937,51.5 L426.894937,51.5 L426.894937,41.45 L407.844937,41.45 L407.844937,20.15 L396.594937,20.15 L396.594937,41.45 L384.444937,41.45 L384.444937,51.5 L396.594937,51.5 L396.594937,101 C396.594937,106.8 398.369937,111.55 401.919937,115.25 C405.469937,118.95 409.994937,120.8 415.494937,120.8 Z M466.694937,120.8 C474.894937,120.8 481.669937,118.65 487.019937,114.35 C492.369937,110.05 495.044937,104.4 495.044937,97.4 C495.044937,92.8 493.894937,89.075 491.594937,86.225 C489.294937,83.375 486.444937,81.15 483.044937,79.55 C479.644937,77.95 475.194937,76.3 469.694937,74.6 C464.094937,72.8 459.994937,71.35 457.394937,70.25 C454.794937,69.15 452.844937,67.9 451.544937,66.5 C450.244937,65.1 449.594937,63.3 449.594937,61.1 C449.594937,57.5 451.094937,54.7 454.094937,52.7 C457.094937,50.7 460.894937,49.7 465.494937,49.7 C472.694937,49.7 480.144937,52.15 487.844937,57.05 L487.844937,57.05 L493.244937,48.35 C489.044937,45.65 484.544937,43.525 479.744937,41.975 C474.944937,40.425 470.194937,39.65 465.494937,39.65 C457.594937,39.65 451.094937,41.725 445.994937,45.875 C440.894937,50.025 438.344937,55.55 438.344937,62.45 C438.344937,67.95 440.294937,72.325 444.194937,75.575 C448.094937,78.825 454.794937,81.8 464.294937,84.5 C468.794937,85.8 472.319937,86.95 474.869937,87.95 C477.419937,88.95 479.544937,90.325 481.244937,92.075 C482.944937,93.825 483.794937,96 483.794937,98.6 C483.794937,102.3 482.219937,105.25 479.069937,107.45 C475.919937,109.65 471.794937,110.75 466.694937,110.75 C458.094937,110.75 449.344937,107.5 440.444937,101 L440.444937,101 L434.594937,109.25 C439.194937,112.95 444.319937,115.8 449.969937,117.8 C455.619937,119.8 461.194937,120.8 466.694937,120.8 Z" id="LNbits" fill="#FFFFFF"></path>
|
||||
<polygon id="Path" fill="#FF1EE6" points="31.7629055 0 7.10760662 78.8585043 26.9789129 78.8585043 2.69673524e-13 148 74.8401889 57.3517272 43.9349963 57.3517272 84.3037975 8.34401992e-14"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
7
logo/muun.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
<svg width="124" height="48" viewBox="0 0 124 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.1236 33.8511V18.2948C11.1236 18.2948 12.5211 17.5999 15.1918 17.5999C17.8624 17.5999 19.0524 18.5926 19.0524 21.3862V33.8511H22.8854V21.3862C22.8854 20.1241 22.8854 19.1598 22.8716 18.4224C23.4804 18.082 24.6704 17.5999 26.6354 17.5999C29.6796 17.5999 30.8143 18.5926 30.8143 21.3862V33.8511H34.9517V21.3862C34.9517 19.5001 34.5642 17.529 33.1805 16.0967C31.7829 14.6645 29.7903 13.7144 26.6769 13.7144C23.8817 13.7144 21.4186 14.6928 20.9343 14.9055C19.3707 14.154 17.1705 13.7144 15.2195 13.7144C11.428 13.7144 7 14.9481 7 14.9481V33.8511H11.1236Z" fill="#3970DB"/>
|
||||
<path d="M62.2121 13.7144V31.4245V32.6174C62.2121 32.6174 61.7388 32.7475 60.9502 32.9231C59.31 33.2885 56.306 33.8511 53.359 33.8511C50.2041 33.8511 47.6885 32.8868 46.2848 31.4687C44.8811 30.0365 44.5058 28.0653 44.5058 26.1793V13.7144H48.6614V26.1651C48.6614 28.9587 50.3292 29.9514 53.3868 29.9514C56.4444 29.9514 58.0705 29.2565 58.0705 29.2565V13.7144H62.2121Z" fill="#3970DB"/>
|
||||
<path d="M89.4791 13.7144V32.6174C89.4791 32.6174 84.99 33.8511 80.626 33.8511C77.4711 33.8511 74.9555 32.8868 73.5518 31.4687C72.1481 30.0365 71.7729 28.0653 71.7729 26.1793V13.7144H75.9284V26.1651C75.9284 28.9587 77.5962 29.9514 80.6538 29.9514C83.7114 29.9514 85.3375 29.2565 85.3375 29.2565V13.7144H89.4791Z" fill="#3970DB"/>
|
||||
<path d="M99.008 33.8511V14.9481C99.008 14.9481 103.497 13.7144 107.861 13.7144C111.016 13.7144 113.532 14.6787 114.935 16.0967C116.339 17.529 116.714 19.5001 116.714 21.3862V33.8511H112.559V21.4004C112.559 18.6067 110.891 17.6141 107.833 17.6141C104.776 17.6141 103.15 18.3089 103.15 18.3089V33.8511H99.008Z" fill="#3970DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
20
logo/strike.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="79" height="20" viewBox="0 0 90 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Strike Icon -->
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.31317 8.49362C2.82011 8.30435 2.57384 7.75121 2.76311 7.25815C2.95238 6.76509 3.50552 6.51881 3.99858 6.70808L7.55779 8.07434C7.56174 8.07591 7.5657 8.07746 7.56967 8.07899L9.35521 8.76439C9.84827 8.95366 10.4014 8.70738 10.5907 8.21432C10.7799 7.72126 10.5337 7.16812 10.0406 6.97885L10.0404 6.97877C10.0405 6.97874 10.0405 6.9787 10.0406 6.97866L8.25506 6.29326C7.762 6.104 7.51572 5.55086 7.70499 5.0578C7.89426 4.56473 8.4474 4.31846 8.94046 4.50773L14.1921 6.52363C14.2046 6.52845 14.217 6.53352 14.2293 6.53881C14.2519 6.54711 14.2745 6.55559 14.2971 6.56425C16.7624 7.5106 17.9938 10.2763 17.0474 12.7416C16.1011 15.2069 13.3354 16.4383 10.8701 15.4919C10.8226 15.4737 10.7756 15.4548 10.729 15.4353C10.7065 15.4285 10.684 15.4208 10.6616 15.4122L5.51344 13.436C5.02038 13.2467 4.77411 12.6936 4.96338 12.2005C5.15265 11.7075 5.70579 11.4612 6.19885 11.6504L7.98415 12.3358C7.98411 12.3356 7.98407 12.3355 7.98403 12.3354L7.9844 12.3355C8.47746 12.5248 9.0306 12.2785 9.21987 11.7854C9.40914 11.2924 9.16287 10.7392 8.66981 10.55L7.86633 10.2416C7.86634 10.2415 7.86634 10.2415 7.86635 10.2414L3.31317 8.49362Z" fill="white"/>
|
||||
|
||||
<!-- Strike Text -->
|
||||
<g transform="translate(28, 3.5)">
|
||||
<!-- S -->
|
||||
<path d="M5.53551 12.0094C4.61482 12.0289 3.7007 11.8537 2.85607 11.496C2.12816 11.1794 1.50979 10.6642 1.0754 10.0128C0.642748 9.33714 0.39054 8.56665 0.341797 7.77161V7.65141H2.808V7.75532C2.82559 8.19609 2.9777 8.62173 3.24484 8.97777C3.50828 9.31505 3.85779 9.57915 4.25853 9.74381C4.69434 9.92241 5.1631 10.0119 5.63583 10.0066C6.02766 10.0134 6.41822 9.96115 6.79372 9.85179C7.0883 9.76915 7.35238 9.60581 7.55452 9.38117C7.73891 9.15386 7.83442 8.87007 7.82409 8.58046C7.83246 8.40646 7.79905 8.23297 7.72648 8.07379C7.65391 7.91462 7.54418 7.77416 7.40609 7.66363C6.99964 7.38173 6.52925 7.19998 6.03506 7.13391L4.07876 6.76512C3.18984 6.62487 2.36041 6.2404 1.68777 5.65679C1.38402 5.37934 1.14449 5.04189 0.985188 4.66701C0.825883 4.29213 0.750487 3.88843 0.763989 3.48287C0.75674 2.83194 0.96648 2.19632 1.36171 1.67161C1.78712 1.12081 2.35771 0.692727 3.01282 0.432882C4.56833 -0.167745 6.30236 -0.15605 7.8492 0.465487C8.50592 0.751173 9.0661 1.21249 9.46479 1.79592C9.84918 2.39288 10.0654 3.07803 10.0918 3.78239V3.90055H7.62764V3.7946C7.60996 3.47278 7.51168 3.16009 7.34133 2.88388C7.15141 2.59851 6.87957 2.37389 6.55966 2.23802C6.13898 2.06192 5.68381 1.97778 5.22619 1.9915C4.85647 1.98677 4.48852 2.04184 4.13729 2.15448C3.85408 2.24116 3.59903 2.39851 3.39741 2.61088C3.22433 2.80318 3.13182 3.05191 3.13824 3.30766C3.12548 3.46769 3.15763 3.6281 3.23126 3.77167C3.3049 3.91524 3.41726 4.03655 3.55625 4.12262C3.9988 4.36024 4.48046 4.5209 4.97958 4.59735L6.91914 4.96407C7.51097 5.06887 8.08087 5.26853 8.60577 5.55492C9.08683 5.82022 9.49099 6.20017 9.78039 6.6592C10.0808 7.15145 10.2323 7.71652 10.2172 8.28912C10.2286 8.97267 10.037 9.64484 9.66543 10.2247C9.2711 10.8075 8.71219 11.2668 8.05611 11.5469C7.2601 11.8769 6.40013 12.0347 5.53551 12.0094Z" fill="white"/>
|
||||
<!-- T -->
|
||||
<path d="M14.7421 11.7221V2.28284H10.8965V0.282107H20.9558V2.28284H17.1248V11.7221H14.7421Z" fill="white"/>
|
||||
<!-- R -->
|
||||
<path d="M28.9273 11.7221V8.86977C28.9273 8.24225 28.7977 7.79606 28.5427 7.54749C28.2877 7.29893 27.8174 7.17058 27.1507 7.17058H24.8517V11.7221H22.469V0.282107H27.6795C28.8164 0.282107 29.7486 0.544927 30.4446 1.0665C31.1406 1.58808 31.5084 2.35823 31.5084 3.34229C31.5294 4.01366 31.2906 4.66816 30.8396 5.17595C30.4931 5.55368 30.0648 5.85154 29.5856 6.04797C30.065 6.18006 30.486 6.46301 30.7832 6.85273C31.1649 7.38962 31.3551 8.03387 31.3245 8.6864V11.7303L28.9273 11.7221ZM27.2677 5.15355C27.6115 5.16251 27.9538 5.10574 28.2751 4.98646C28.5168 4.89368 28.7223 4.72901 28.8625 4.51584C29.0029 4.27557 29.0709 4.00159 29.0589 3.72532C29.0748 3.52488 29.0431 3.32355 28.9663 3.13698C28.8894 2.95042 28.7695 2.78363 28.6158 2.64957C28.3148 2.40712 27.8613 2.28283 27.2677 2.28283H24.8496V5.15355H27.2677Z" fill="white"/>
|
||||
<!-- I -->
|
||||
<path d="M36.2633 0.282107H33.8806V11.7221H36.2633V0.282107Z" fill="white"/>
|
||||
<!-- K -->
|
||||
<path d="M46.0405 11.7058L42.1697 6.72642L41.1644 7.74512V11.7058H38.8152V0.263777H41.1644V4.77663L41.2585 4.66252L41.4675 4.4221C41.5455 4.33517 41.6291 4.24758 41.7183 4.15929L45.4804 0.263777H48.5485L43.7748 5.09853L49.1755 11.7058H46.0405Z" fill="white"/>
|
||||
<!-- E -->
|
||||
<path d="M50.0638 11.7038V0.263771H58.2985V2.26654H52.4464V4.78273H58.0519V6.79976H52.4464V9.70306H58.5452V11.7038H50.0638Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
1
logo/wos.svg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
14
logo/zeus.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 258.12 100">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #ffa900;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path class="cls-1" d="m13.83,27.6L0,0h46.47C29.48,4.39,19.86,16.16,13.83,27.6Zm12.86,72.4h46.47l-13.83-27.6c-6.03,11.43-15.65,23.21-32.64,27.6ZM50.12,0L0,100h23.05L73.17,0h-23.05Zm59.33,27.59L123.28,0h-46.47c16.99,4.39,26.62,16.16,32.64,27.6Zm-32.64,72.4h46.47l-13.83-27.6c-6.03,11.43-15.65,23.21-32.64,27.6Zm-3.14-89.97l-11.52,22.98,8.51,16.99-8.51,16.98,11.52,22.98,20.03-39.96-20.03-39.97ZM179.43.01h-23.05s25.06,49.99,25.06,49.99l-25.06,50h23.04l25.06-50L179.43.01Zm-75.68,49.99l25.06,50h23.04s-25.06-50-25.06-50L151.85,0h-23.04s-25.06,50-25.06,50Zm140.53-22.4L258.12,0h-46.47c16.99,4.39,26.62,16.16,32.64,27.6Zm-45.5,44.81l-13.83,27.6h46.47c-16.99-4.39-26.62-16.16-32.64-27.6ZM184.95,0l50.12,100h23.05S208.01.01,208.01.01h-23.06Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
291
src/components/ui/Header.jsx
Normal file
@@ -0,0 +1,291 @@
|
||||
const EnhancedMinimalHeader = ({
|
||||
status,
|
||||
fingerprint,
|
||||
verificationCode,
|
||||
onDisconnect,
|
||||
isConnected,
|
||||
securityLevel,
|
||||
sessionManager,
|
||||
sessionTimeLeft
|
||||
}) => {
|
||||
const [currentTimeLeft, setCurrentTimeLeft] = React.useState(sessionTimeLeft || 0);
|
||||
const [hasActiveSession, setHasActiveSession] = React.useState(false);
|
||||
const [sessionType, setSessionType] = React.useState('unknown');
|
||||
|
||||
React.useEffect(() => {
|
||||
const updateSessionInfo = () => {
|
||||
if (sessionManager) {
|
||||
const isActive = sessionManager.hasActiveSession();
|
||||
const timeLeft = sessionManager.getTimeLeft();
|
||||
const currentSession = sessionManager.currentSession;
|
||||
|
||||
setHasActiveSession(isActive);
|
||||
setCurrentTimeLeft(timeLeft);
|
||||
setSessionType(currentSession?.type || 'unknown');
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
updateSessionInfo();
|
||||
|
||||
const interval = setInterval(updateSessionInfo, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [sessionManager]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (sessionManager?.hasActiveSession()) {
|
||||
setCurrentTimeLeft(sessionManager.getTimeLeft());
|
||||
setHasActiveSession(true);
|
||||
} else {
|
||||
setHasActiveSession(false);
|
||||
}
|
||||
}, [sessionManager, sessionTimeLeft]);
|
||||
|
||||
const handleSecurityClick = () => {
|
||||
if (securityLevel?.verificationResults) {
|
||||
alert('Security check details:\n\n' +
|
||||
Object.entries(securityLevel.verificationResults)
|
||||
.map(([key, result]) => `${key}: ${result.passed ? '✅' : '❌'} ${result.details}`)
|
||||
.join('\n')
|
||||
);
|
||||
} else if (securityLevel) {
|
||||
alert(`Security Level: ${securityLevel.level}\nScore: ${securityLevel.score}%\nDetails: ${securityLevel.details || 'No additional details available'}`);
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowTimer = hasActiveSession && currentTimeLeft > 0 && window.SessionTimer;
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleForceUpdate = (event) => {
|
||||
|
||||
if (sessionManager) {
|
||||
const isActive = sessionManager.hasActiveSession();
|
||||
const timeLeft = sessionManager.getTimeLeft();
|
||||
const currentSession = sessionManager.currentSession;
|
||||
|
||||
setHasActiveSession(isActive);
|
||||
setCurrentTimeLeft(timeLeft);
|
||||
setSessionType(currentSession?.type || 'unknown');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('force-header-update', handleForceUpdate);
|
||||
return () => document.removeEventListener('force-header-update', handleForceUpdate);
|
||||
}, [sessionManager]);
|
||||
|
||||
const getStatusConfig = () => {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return {
|
||||
text: 'Connected',
|
||||
className: 'status-connected',
|
||||
badgeClass: 'bg-green-500/10 text-green-400 border-green-500/20'
|
||||
};
|
||||
case 'verifying':
|
||||
return {
|
||||
text: 'Verifying...',
|
||||
className: 'status-verifying',
|
||||
badgeClass: 'bg-purple-500/10 text-purple-400 border-purple-500/20'
|
||||
};
|
||||
case 'connecting':
|
||||
return {
|
||||
text: 'Connecting...',
|
||||
className: 'status-connecting',
|
||||
badgeClass: 'bg-blue-500/10 text-blue-400 border-blue-500/20'
|
||||
};
|
||||
case 'retrying':
|
||||
return {
|
||||
text: 'Retrying...',
|
||||
className: 'status-connecting',
|
||||
badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
text: 'Error',
|
||||
className: 'status-failed',
|
||||
badgeClass: 'bg-red-500/10 text-red-400 border-red-500/20'
|
||||
};
|
||||
case 'reconnecting':
|
||||
return {
|
||||
text: 'Reconnecting...',
|
||||
className: 'status-connecting',
|
||||
badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
|
||||
};
|
||||
case 'peer_disconnected':
|
||||
return {
|
||||
text: 'Peer disconnected',
|
||||
className: 'status-failed',
|
||||
badgeClass: 'bg-orange-500/10 text-orange-400 border-orange-500/20'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: 'Not connected',
|
||||
className: 'status-disconnected',
|
||||
badgeClass: 'bg-gray-500/10 text-gray-400 border-gray-500/20'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getStatusConfig();
|
||||
|
||||
return React.createElement('header', {
|
||||
className: 'header-minimal sticky top-0 z-50'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'container',
|
||||
className: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'content',
|
||||
className: 'flex items-center justify-between h-16'
|
||||
}, [
|
||||
// Logo and Title
|
||||
React.createElement('div', {
|
||||
key: 'logo-section',
|
||||
className: 'flex items-center space-x-2 sm:space-x-3'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'logo',
|
||||
className: 'icon-container w-8 h-8 sm:w-10 sm:h-10'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-shield-halved accent-orange text-sm sm:text-base'
|
||||
})
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'title-section'
|
||||
}, [
|
||||
React.createElement('h1', {
|
||||
key: 'title',
|
||||
className: 'text-lg sm:text-xl font-semibold text-primary'
|
||||
}, 'SecureBit.chat'),
|
||||
React.createElement('p', {
|
||||
key: 'subtitle',
|
||||
className: 'text-xs sm:text-sm text-muted hidden sm:block'
|
||||
}, 'End-to-end freedom. v4.0.03.00')
|
||||
])
|
||||
]),
|
||||
|
||||
// Status and Controls - Responsive
|
||||
React.createElement('div', {
|
||||
key: 'status-section',
|
||||
className: 'flex items-center space-x-2 sm:space-x-3'
|
||||
}, [
|
||||
// Session Timer
|
||||
shouldShowTimer && React.createElement(window.SessionTimer, {
|
||||
key: 'session-timer',
|
||||
timeLeft: currentTimeLeft,
|
||||
sessionType: sessionType,
|
||||
sessionManager: sessionManager
|
||||
}),
|
||||
|
||||
// Security Level Indicator
|
||||
securityLevel && React.createElement('div', {
|
||||
key: 'security-level',
|
||||
className: 'hidden md:flex items-center space-x-2 cursor-pointer hover:opacity-80 transition-opacity duration-200',
|
||||
onClick: handleSecurityClick,
|
||||
title: `${securityLevel.level} (${securityLevel.score}%) - Click for details`
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'security-icon',
|
||||
className: `w-6 h-6 rounded-full flex items-center justify-center ${
|
||||
securityLevel.color === 'green' ? 'bg-green-500/20' :
|
||||
securityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
|
||||
}`
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: `fas fa-shield-alt text-xs ${
|
||||
securityLevel.color === 'green' ? 'text-green-400' :
|
||||
securityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400'
|
||||
}`
|
||||
})
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'security-info',
|
||||
className: 'flex flex-col'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'security-level-text',
|
||||
className: 'text-xs font-medium text-primary'
|
||||
}, `${securityLevel.level} (${securityLevel.score}%)`),
|
||||
securityLevel.details && React.createElement('div', {
|
||||
key: 'security-details',
|
||||
className: 'text-xs text-muted mt-1 hidden lg:block'
|
||||
}, securityLevel.details),
|
||||
React.createElement('div', {
|
||||
key: 'security-progress',
|
||||
className: 'w-16 h-1 bg-gray-600 rounded-full overflow-hidden'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-bar',
|
||||
className: `h-full transition-all duration-500 ${
|
||||
securityLevel.color === 'green' ? 'bg-green-400' :
|
||||
securityLevel.color === 'yellow' ? 'bg-yellow-400' : 'bg-red-400'
|
||||
}`,
|
||||
style: { width: `${securityLevel.score}%` }
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Mobile Security Indicator
|
||||
securityLevel && React.createElement('div', {
|
||||
key: 'mobile-security',
|
||||
className: 'md:hidden flex items-center'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'mobile-security-icon',
|
||||
className: `w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity duration-200 ${
|
||||
securityLevel.color === 'green' ? 'bg-green-500/20' :
|
||||
securityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
|
||||
}`,
|
||||
title: `${securityLevel.level} (${securityLevel.score}%) - Click for details`,
|
||||
onClick: handleSecurityClick
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: `fas fa-shield-alt text-sm ${
|
||||
securityLevel.color === 'green' ? 'text-green-400' :
|
||||
securityLevel.color === 'yellow' ? 'text-yellow-400' : 'bg-red-400'
|
||||
}`
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Status Badge
|
||||
React.createElement('div', {
|
||||
key: 'status-badge',
|
||||
className: `px-2 sm:px-3 py-1.5 rounded-lg border ${config.badgeClass} flex items-center space-x-1 sm:space-x-2`
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'status-dot',
|
||||
className: `status-dot ${config.className}`
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'status-text',
|
||||
className: 'text-xs sm:text-sm font-medium'
|
||||
}, config.text)
|
||||
]),
|
||||
|
||||
// Disconnect Button
|
||||
isConnected && React.createElement('button', {
|
||||
key: 'disconnect-btn',
|
||||
onClick: onDisconnect,
|
||||
className: 'p-1.5 sm:px-3 sm:py-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 rounded-lg transition-all duration-200 text-sm'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-power-off sm:mr-2'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
className: 'hidden sm:inline'
|
||||
}, 'Disconnect')
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.EnhancedMinimalHeader = EnhancedMinimalHeader;
|
||||
|
||||
console.log('✅ EnhancedMinimalHeader loaded with timer fixes');
|
||||
372
src/components/ui/LightningPayment.jsx
Normal file
@@ -0,0 +1,372 @@
|
||||
const React = window.React;
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
const IntegratedLightningPayment = ({ sessionType, onSuccess, onCancel, paymentManager }) => {
|
||||
const [paymentMethod, setPaymentMethod] = useState('webln');
|
||||
const [preimage, setPreimage] = useState('');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [invoice, setInvoice] = useState(null);
|
||||
const [paymentStatus, setPaymentStatus] = useState('pending'); // pending, created, paid, expired
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
createInvoice();
|
||||
}, [sessionType]);
|
||||
|
||||
const createInvoice = async () => {
|
||||
if (sessionType === 'free') {
|
||||
setPaymentStatus('free');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
|
||||
if (!paymentManager) {
|
||||
throw new Error('Payment manager not available. Please check sessionManager initialization.');
|
||||
}
|
||||
|
||||
const createdInvoice = await paymentManager.createLightningInvoice(sessionType);
|
||||
|
||||
if (!createdInvoice) {
|
||||
throw new Error('Failed to create invoice');
|
||||
}
|
||||
|
||||
setInvoice(createdInvoice);
|
||||
setPaymentStatus('created');
|
||||
|
||||
if (createdInvoice.paymentRequest) {
|
||||
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(createdInvoice.paymentRequest)}`;
|
||||
setQrCodeUrl(qrUrl);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Invoice creation failed:', err);
|
||||
setError(`Error creating invoice: ${err.message}`);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebLNPayment = async () => {
|
||||
if (!window.webln) {
|
||||
setError('WebLN is not supported. Please use the Alby or Zeus wallet.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!invoice || !invoice.paymentRequest) {
|
||||
setError('Invoice is not ready for payment');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await window.webln.enable();
|
||||
|
||||
const result = await window.webln.sendPayment(invoice.paymentRequest);
|
||||
|
||||
if (result.preimage) {
|
||||
setPaymentStatus('paid');
|
||||
await activateSession(result.preimage);
|
||||
} else {
|
||||
setError('Payment does not contain preimage');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('WebLN payment failed:', err);
|
||||
setError(`WebLN Error: ${err.message}`);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualVerification = async () => {
|
||||
const trimmedPreimage = preimage.trim();
|
||||
|
||||
if (!trimmedPreimage) {
|
||||
setError('Enter payment preimage');
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedPreimage.length !== 64) {
|
||||
setError('The preimage must be exactly 64 characters long.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(trimmedPreimage)) {
|
||||
setError('The preimage must contain only hexadecimal characters (0-9, a-f, A-F).');
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedPreimage === '1'.repeat(64) ||
|
||||
trimmedPreimage === 'a'.repeat(64) ||
|
||||
trimmedPreimage === 'f'.repeat(64)) {
|
||||
setError('The entered preimage is too weak. Please verify the key..');
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
await activateSession(trimmedPreimage);
|
||||
} catch (err) {
|
||||
setError(`Activation error: ${err.message}`);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const activateSession = async (preimageValue) => {
|
||||
try {
|
||||
|
||||
let result;
|
||||
if (paymentManager) {
|
||||
const paymentHash = invoice?.paymentHash || 'dummy_hash';
|
||||
result = await paymentManager.safeActivateSession(sessionType, preimageValue, paymentHash);
|
||||
} else {
|
||||
console.warn('Payment manager not available, using fallback');
|
||||
// Fallback if paymentManager is unavailable
|
||||
result = { success: true, method: 'fallback' };
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
setPaymentStatus('paid');
|
||||
onSuccess(preimageValue, invoice);
|
||||
} else {
|
||||
console.error('❌ Session activation failed:', result);
|
||||
throw new Error(`Session activation failed: ${result.reason}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Session activation failed:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFreeSession = async () => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await activateSession('0'.repeat(64));
|
||||
} catch (err) {
|
||||
setError(`Free session activation error: ${err.message}`);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const pricing = {
|
||||
free: { sats: 1, hours: 1/60 },
|
||||
basic: { sats: 500, hours: 1 },
|
||||
premium: { sats: 1000, hours: 4 },
|
||||
extended: { sats: 2000, hours: 24 }
|
||||
}[sessionType];
|
||||
|
||||
return React.createElement('div', { className: 'space-y-4 max-w-md mx-auto' }, [
|
||||
React.createElement('div', { key: 'header', className: 'text-center' }, [
|
||||
React.createElement('h3', {
|
||||
key: 'title',
|
||||
className: 'text-xl font-semibold text-white mb-2'
|
||||
}, sessionType === 'free' ? 'Free session' : 'Lightning payment'),
|
||||
React.createElement('div', {
|
||||
key: 'amount',
|
||||
className: 'text-2xl font-bold text-orange-400'
|
||||
}, sessionType === 'free'
|
||||
? '0 sat per minute'
|
||||
: `${pricing.sats} сат за ${pricing.hours}ч`
|
||||
),
|
||||
sessionType !== 'free' && React.createElement('div', {
|
||||
key: 'usd',
|
||||
className: 'text-sm text-gray-400 mt-1'
|
||||
}, `≈ $${(pricing.sats * 0.0004).toFixed(2)} USD`)
|
||||
]),
|
||||
|
||||
// Loading State
|
||||
isProcessing && paymentStatus === 'pending' && React.createElement('div', {
|
||||
key: 'loading',
|
||||
className: 'text-center'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'spinner',
|
||||
className: 'text-orange-400'
|
||||
}, [
|
||||
React.createElement('i', { className: 'fas fa-spinner fa-spin mr-2' }),
|
||||
'Creating invoice...'
|
||||
])
|
||||
]),
|
||||
|
||||
// Free Session
|
||||
sessionType === 'free' && React.createElement('div', {
|
||||
key: 'free-session',
|
||||
className: 'space-y-3'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'info',
|
||||
className: 'p-3 bg-blue-500/10 border border-blue-500/20 rounded text-blue-300 text-sm'
|
||||
}, 'A free 1-minute session will be activated.'),
|
||||
React.createElement('button', {
|
||||
key: 'start-btn',
|
||||
onClick: handleFreeSession,
|
||||
disabled: isProcessing,
|
||||
className: 'w-full bg-blue-600 hover:bg-blue-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-play'} mr-2`
|
||||
}),
|
||||
isProcessing ? 'Activation...' : 'Start free session'
|
||||
])
|
||||
]),
|
||||
|
||||
// Paid Sessions
|
||||
sessionType !== 'free' && paymentStatus === 'created' && invoice && React.createElement('div', {
|
||||
key: 'paid-session',
|
||||
className: 'space-y-4'
|
||||
}, [
|
||||
// QR Code
|
||||
qrCodeUrl && React.createElement('div', {
|
||||
key: 'qr-section',
|
||||
className: 'text-center'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'qr-container',
|
||||
className: 'bg-white p-4 rounded-lg inline-block'
|
||||
}, [
|
||||
React.createElement('img', {
|
||||
key: 'qr-img',
|
||||
src: qrCodeUrl,
|
||||
alt: 'Payment QR Code',
|
||||
className: 'w-48 h-48'
|
||||
})
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'qr-hint',
|
||||
className: 'text-xs text-gray-400 mt-2'
|
||||
}, 'Scan the QR code with any Lightning wallet')
|
||||
]),
|
||||
|
||||
// Payment Request
|
||||
invoice.paymentRequest && React.createElement('div', {
|
||||
key: 'payment-request',
|
||||
className: 'space-y-2'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'label',
|
||||
className: 'text-sm font-medium text-white'
|
||||
}, 'Payment Request:'),
|
||||
React.createElement('div', {
|
||||
key: 'request',
|
||||
className: 'p-3 bg-gray-800 rounded border text-xs font-mono text-gray-300 cursor-pointer hover:bg-gray-700',
|
||||
onClick: () => copyToClipboard(invoice.paymentRequest)
|
||||
}, [
|
||||
invoice.paymentRequest.substring(0, 50) + '...',
|
||||
React.createElement('i', { key: 'copy-icon', className: 'fas fa-copy ml-2 text-orange-400' })
|
||||
])
|
||||
]),
|
||||
|
||||
// WebLN Payment
|
||||
React.createElement('div', {
|
||||
key: 'webln-section',
|
||||
className: 'space-y-3'
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'webln-title',
|
||||
className: 'text-white font-medium flex items-center'
|
||||
}, [
|
||||
React.createElement('i', { key: 'bolt-icon', className: 'fas fa-bolt text-orange-400 mr-2' }),
|
||||
'WebLN wallet (Alby, Zeus)'
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'webln-btn',
|
||||
onClick: handleWebLNPayment,
|
||||
disabled: isProcessing,
|
||||
className: 'w-full bg-orange-600 hover:bg-orange-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'webln-icon',
|
||||
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-bolt'} mr-2`
|
||||
}),
|
||||
isProcessing ? 'Processing...' : 'Pay via WebLN'
|
||||
])
|
||||
]),
|
||||
|
||||
// Manual Payment
|
||||
React.createElement('div', {
|
||||
key: 'divider',
|
||||
className: 'text-center text-gray-400'
|
||||
}, 'or'),
|
||||
|
||||
React.createElement('div', {
|
||||
key: 'manual-section',
|
||||
className: 'space-y-3'
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'manual-title',
|
||||
className: 'text-white font-medium'
|
||||
}, 'Manual payment verification'),
|
||||
React.createElement('input', {
|
||||
key: 'preimage-input',
|
||||
type: 'text',
|
||||
value: preimage,
|
||||
onChange: (e) => setPreimage(e.target.value),
|
||||
placeholder: 'Enter the preimage after payment...',
|
||||
className: 'w-full p-3 bg-gray-800 border border-gray-600 rounded text-white placeholder-gray-400 text-sm'
|
||||
}),
|
||||
React.createElement('button', {
|
||||
key: 'verify-btn',
|
||||
onClick: handleManualVerification,
|
||||
disabled: isProcessing,
|
||||
className: 'w-full bg-green-600 hover:bg-green-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'verify-icon',
|
||||
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-check'} mr-2`
|
||||
}),
|
||||
isProcessing ? 'Verification...' : 'Confirm payment'
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Success State
|
||||
paymentStatus === 'paid' && React.createElement('div', {
|
||||
key: 'success',
|
||||
className: 'text-center p-4 bg-green-500/10 border border-green-500/20 rounded'
|
||||
}, [
|
||||
React.createElement('i', { key: 'success-icon', className: 'fas fa-check-circle text-green-400 text-2xl mb-2' }),
|
||||
React.createElement('div', { key: 'success-text', className: 'text-green-300 font-medium' }, 'Payment confirmed!'),
|
||||
React.createElement('div', { key: 'success-subtext', className: 'text-green-400 text-sm' }, 'Session activated')
|
||||
]),
|
||||
|
||||
// Error State
|
||||
error && React.createElement('div', {
|
||||
key: 'error',
|
||||
className: 'p-3 bg-red-500/10 border border-red-500/20 rounded text-red-400 text-sm'
|
||||
}, [
|
||||
React.createElement('i', { key: 'error-icon', className: 'fas fa-exclamation-triangle mr-2' }),
|
||||
error,
|
||||
error.includes('invoice') && React.createElement('button', {
|
||||
key: 'retry-btn',
|
||||
onClick: createInvoice,
|
||||
className: 'ml-2 text-orange-400 hover:text-orange-300 underline'
|
||||
}, 'Try again')
|
||||
]),
|
||||
|
||||
// Cancel Button
|
||||
React.createElement('button', {
|
||||
key: 'cancel-btn',
|
||||
onClick: onCancel,
|
||||
className: 'w-full bg-gray-600 hover:bg-gray-500 text-white py-2 px-4 rounded'
|
||||
}, 'Cancel')
|
||||
]);
|
||||
};
|
||||
|
||||
window.LightningPayment = IntegratedLightningPayment;
|
||||
91
src/components/ui/PasswordModal.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
const React = window.React;
|
||||
|
||||
const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPassword }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (password.trim()) {
|
||||
onSubmit(password.trim());
|
||||
setPassword('');
|
||||
}
|
||||
};
|
||||
|
||||
const getActionText = () => {
|
||||
return action === 'offer' ? 'invitation' : 'response';
|
||||
};
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'modal',
|
||||
className: 'card-minimal rounded-xl p-6 max-w-md w-full border-purple-500/20'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: 'flex items-center mb-4'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'icon',
|
||||
className: 'w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-key accent-purple'
|
||||
})
|
||||
]),
|
||||
React.createElement('h3', {
|
||||
key: 'title',
|
||||
className: 'text-lg font-medium text-primary'
|
||||
}, 'Password input')
|
||||
]),
|
||||
React.createElement('form', {
|
||||
key: 'form',
|
||||
onSubmit: handleSubmit,
|
||||
className: 'space-y-4'
|
||||
}, [
|
||||
React.createElement('p', {
|
||||
key: 'description',
|
||||
className: 'text-secondary text-sm'
|
||||
}, `Enter password for decryption ${getActionText()}:`),
|
||||
React.createElement('input', {
|
||||
key: 'password-input',
|
||||
type: 'password',
|
||||
value: password,
|
||||
onChange: (e) => setPassword(e.target.value),
|
||||
placeholder: 'Enter password...',
|
||||
className: 'w-full p-3 bg-gray-900/30 border border-gray-500/20 rounded-lg text-primary placeholder-gray-500 focus:border-purple-500/40 focus:outline-none transition-all',
|
||||
autoFocus: true
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'buttons',
|
||||
className: 'flex space-x-3'
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'submit',
|
||||
type: 'submit',
|
||||
className: 'flex-1 btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-unlock-alt mr-2'
|
||||
}),
|
||||
'Decrypt'
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'cancel',
|
||||
type: 'button',
|
||||
onClick: onClose,
|
||||
className: 'flex-1 btn-secondary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-times mr-2'
|
||||
}),
|
||||
'Cancel'
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.PasswordModal = PasswordModal;
|
||||
600
src/components/ui/PaymentModal.jsx
Normal file
@@ -0,0 +1,600 @@
|
||||
const React = window.React;
|
||||
const { useState, useEffect, useRef } = React;
|
||||
|
||||
const PaymentModal = ({ isOpen, onClose, sessionManager, onSessionPurchased }) => {
|
||||
const [step, setStep] = useState('select');
|
||||
const [selectedType, setSelectedType] = useState(null);
|
||||
const [invoice, setInvoice] = useState(null);
|
||||
const [paymentStatus, setPaymentStatus] = useState('pending');
|
||||
const [error, setError] = useState('');
|
||||
const [paymentMethod, setPaymentMethod] = useState('webln');
|
||||
const [preimageInput, setPreimageInput] = useState('');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState('');
|
||||
const [paymentTimer, setPaymentTimer] = useState(null);
|
||||
const [timeLeft, setTimeLeft] = useState(0);
|
||||
const pollInterval = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
resetModal();
|
||||
if (pollInterval.current) {
|
||||
clearInterval(pollInterval.current);
|
||||
}
|
||||
if (paymentTimer) {
|
||||
clearInterval(paymentTimer);
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const resetModal = () => {
|
||||
setStep('select');
|
||||
setSelectedType(null);
|
||||
setInvoice(null);
|
||||
setPaymentStatus('pending');
|
||||
setError('');
|
||||
setPaymentMethod('webln');
|
||||
setPreimageInput('');
|
||||
setIsProcessing(false);
|
||||
setQrCodeUrl('');
|
||||
setTimeLeft(0);
|
||||
};
|
||||
|
||||
const handleSelectType = async (type) => {
|
||||
setSelectedType(type);
|
||||
setError('');
|
||||
|
||||
if (type === 'demo') {
|
||||
// Создаем demo сессию
|
||||
try {
|
||||
if (!sessionManager || !sessionManager.createDemoSession) {
|
||||
throw new Error('Demo session manager not available');
|
||||
}
|
||||
|
||||
const demoSession = sessionManager.createDemoSession();
|
||||
if (!demoSession.success) {
|
||||
throw new Error(demoSession.reason);
|
||||
}
|
||||
|
||||
setInvoice({
|
||||
sessionType: 'demo',
|
||||
amount: 0,
|
||||
paymentHash: demoSession.paymentHash,
|
||||
memo: `Demo session (${demoSession.durationMinutes} minutes)`,
|
||||
createdAt: Date.now(),
|
||||
isDemo: true,
|
||||
preimage: demoSession.preimage,
|
||||
warning: demoSession.warning
|
||||
});
|
||||
setPaymentStatus('demo');
|
||||
} catch (error) {
|
||||
setError(`Demo session creation failed: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await createRealInvoice(type);
|
||||
}
|
||||
setStep('payment');
|
||||
};
|
||||
|
||||
const createRealInvoice = async (type) => {
|
||||
setPaymentStatus('creating');
|
||||
setIsProcessing(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
console.log(`Creating real Lightning invoice for ${type} session...`);
|
||||
|
||||
if (!sessionManager) {
|
||||
throw new Error('Session manager not initialized');
|
||||
}
|
||||
|
||||
const createdInvoice = await sessionManager.createLightningInvoice(type);
|
||||
|
||||
if (!createdInvoice || !createdInvoice.paymentRequest) {
|
||||
throw new Error('Failed to create Lightning invoice');
|
||||
}
|
||||
|
||||
setInvoice(createdInvoice);
|
||||
setPaymentStatus('created');
|
||||
|
||||
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(createdInvoice.paymentRequest)}`;
|
||||
setQrCodeUrl(qrUrl);
|
||||
|
||||
const expirationTime = 15 * 60 * 1000;
|
||||
setTimeLeft(expirationTime);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setTimeLeft(prev => {
|
||||
const newTime = prev - 1000;
|
||||
if (newTime <= 0) {
|
||||
clearInterval(timer);
|
||||
setPaymentStatus('expired');
|
||||
setError('Payment time has expired. Create a new invoice.');
|
||||
return 0;
|
||||
}
|
||||
return newTime;
|
||||
});
|
||||
}, 1000);
|
||||
setPaymentTimer(timer);
|
||||
|
||||
startPaymentPolling(createdInvoice.checkingId);
|
||||
|
||||
console.log('✅ Lightning invoice created successfully:', createdInvoice);
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Invoice creation failed:', err);
|
||||
setError(`Invoice creation error: ${err.message}`);
|
||||
setPaymentStatus('failed');
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startPaymentPolling = (checkingId) => {
|
||||
if (pollInterval.current) {
|
||||
clearInterval(pollInterval.current);
|
||||
}
|
||||
|
||||
pollInterval.current = setInterval(async () => {
|
||||
try {
|
||||
const status = await sessionManager.checkPaymentStatus(checkingId);
|
||||
|
||||
if (status.paid && status.preimage) {
|
||||
clearInterval(pollInterval.current);
|
||||
setPaymentStatus('paid');
|
||||
await handlePaymentSuccess(status.preimage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Payment status check failed:', error);
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const handleWebLNPayment = async () => {
|
||||
if (!window.webln) {
|
||||
setError('WebLN is not supported. Please install the Alby or Zeus wallet.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!invoice || !invoice.paymentRequest) {
|
||||
setError('Invoice is not ready for payment.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
setError('');
|
||||
setPaymentStatus('paying');
|
||||
|
||||
try {
|
||||
await window.webln.enable();
|
||||
|
||||
const result = await window.webln.sendPayment(invoice.paymentRequest);
|
||||
|
||||
if (result.preimage) {
|
||||
setPaymentStatus('paid');
|
||||
await handlePaymentSuccess(result.preimage);
|
||||
} else {
|
||||
throw new Error('Payment does not contain preimage');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ WebLN payment failed:', err);
|
||||
setError(`WebLN payment error: ${err.message}`);
|
||||
setPaymentStatus('created');
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualVerification = async () => {
|
||||
const trimmedPreimage = preimageInput.trim();
|
||||
|
||||
if (!trimmedPreimage) {
|
||||
setError('Enter payment preimage');
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedPreimage.length !== 64) {
|
||||
setError('The preimage must be exactly 64 characters long.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(trimmedPreimage)) {
|
||||
setError('The preimage must contain only hexadecimal characters (0-9, a-f, A-F).');
|
||||
return;
|
||||
}
|
||||
|
||||
const dummyPreimages = ['1'.repeat(64), 'a'.repeat(64), 'f'.repeat(64), '0'.repeat(64)];
|
||||
if (dummyPreimages.includes(trimmedPreimage) && selectedType !== 'free') {
|
||||
setError('The entered preimage is invalid. Please use the actual preimage from the payment..');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
setError('');
|
||||
setPaymentStatus('paying');
|
||||
|
||||
try {
|
||||
await handlePaymentSuccess(trimmedPreimage);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setPaymentStatus('created');
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDemoSession = async () => {
|
||||
setIsProcessing(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
if (!invoice?.preimage) {
|
||||
throw new Error('Demo preimage not available');
|
||||
}
|
||||
|
||||
// Для demo сессий используем специальную логику верификации
|
||||
const isValid = await sessionManager.verifyPayment(invoice.preimage, invoice.paymentHash);
|
||||
|
||||
if (isValid && isValid.verified) {
|
||||
onSessionPurchased({
|
||||
type: 'demo',
|
||||
preimage: invoice.preimage,
|
||||
paymentHash: invoice.paymentHash,
|
||||
amount: 0,
|
||||
isDemo: true,
|
||||
warning: invoice.warning
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(isValid?.reason || 'Demo session verification failed');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Demo session activation error: ${err.message}`);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaymentSuccess = async (preimage) => {
|
||||
try {
|
||||
console.log('🔍 Verifying payment...', { selectedType, preimage });
|
||||
|
||||
let isValid;
|
||||
if (selectedType === 'demo') {
|
||||
// Demo сессии уже обработаны в handleDemoSession
|
||||
return;
|
||||
} else {
|
||||
isValid = await sessionManager.verifyPayment(preimage, invoice.paymentHash);
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
if (pollInterval.current) {
|
||||
clearInterval(pollInterval.current);
|
||||
}
|
||||
if (paymentTimer) {
|
||||
clearInterval(paymentTimer);
|
||||
}
|
||||
|
||||
onSessionPurchased({
|
||||
type: selectedType,
|
||||
preimage,
|
||||
paymentHash: invoice.paymentHash,
|
||||
amount: invoice.amount
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 1500);
|
||||
|
||||
} else {
|
||||
throw new Error('Payment verification failed. Please check the preimage for correctness or try again.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Payment verification failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (ms) => {
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds = Math.floor((ms % 60000) / 1000);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const pricing = sessionManager?.sessionPrices || {
|
||||
demo: { sats: 0, hours: 0.1, usd: 0.00 },
|
||||
basic: { sats: 500, hours: 1, usd: 0.20 },
|
||||
premium: { sats: 1000, hours: 4, usd: 0.40 },
|
||||
extended: { sats: 2000, hours: 24, usd: 0.80 }
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'modal',
|
||||
className: 'card-minimal rounded-xl p-6 max-w-lg w-full max-h-[90vh] overflow-y-auto custom-scrollbar'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: 'flex items-center justify-between mb-6'
|
||||
}, [
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
className: 'text-xl font-semibold text-primary'
|
||||
}, step === 'select' ? 'Select session type' : 'Session payment'),
|
||||
React.createElement('button', {
|
||||
key: 'close',
|
||||
onClick: onClose,
|
||||
className: 'text-gray-400 hover:text-white transition-colors'
|
||||
}, React.createElement('i', { className: 'fas fa-times' }))
|
||||
]),
|
||||
|
||||
step === 'select' && window.SessionTypeSelector && React.createElement(window.SessionTypeSelector, {
|
||||
key: 'selector',
|
||||
onSelectType: handleSelectType,
|
||||
onCancel: onClose
|
||||
}),
|
||||
|
||||
step === 'payment' && React.createElement('div', {
|
||||
key: 'payment-step',
|
||||
className: 'space-y-6'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'session-info',
|
||||
className: 'text-center p-4 bg-orange-500/10 border border-orange-500/20 rounded-lg'
|
||||
}, [
|
||||
React.createElement('h3', {
|
||||
key: 'session-title',
|
||||
className: 'text-lg font-semibold text-orange-400 mb-2'
|
||||
}, `${selectedType.charAt(0).toUpperCase() + selectedType.slice(1)} session`),
|
||||
React.createElement('div', {
|
||||
key: 'session-details',
|
||||
className: 'text-sm text-secondary'
|
||||
}, [
|
||||
React.createElement('div', { key: 'amount' }, `${pricing[selectedType].sats} sat for ${pricing[selectedType].hours}ч`),
|
||||
pricing[selectedType].usd > 0 && React.createElement('div', {
|
||||
key: 'usd',
|
||||
className: 'text-gray-400'
|
||||
}, `≈ $${pricing[selectedType].usd} USD`)
|
||||
])
|
||||
]),
|
||||
|
||||
timeLeft > 0 && paymentStatus === 'created' && React.createElement('div', {
|
||||
key: 'timer',
|
||||
className: 'text-center p-3 bg-yellow-500/10 border border-yellow-500/20 rounded'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'timer-text',
|
||||
className: 'text-yellow-400 font-medium'
|
||||
}, `⏱️ Time to pay: ${formatTime(timeLeft)}`)
|
||||
]),
|
||||
|
||||
paymentStatus === 'demo' && React.createElement('div', {
|
||||
key: 'demo-payment',
|
||||
className: 'space-y-4'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'demo-info',
|
||||
className: 'p-4 bg-green-500/10 border border-green-500/20 rounded text-green-300 text-sm text-center'
|
||||
}, [
|
||||
React.createElement('div', { key: 'demo-title', className: 'font-medium mb-1' }, '🎮 Demo Session Available'),
|
||||
React.createElement('div', { key: 'demo-details', className: 'text-xs' },
|
||||
`Limited to ${invoice?.durationMinutes || 6} minutes for testing`)
|
||||
]),
|
||||
invoice?.warning && React.createElement('div', {
|
||||
key: 'demo-warning',
|
||||
className: 'p-3 bg-yellow-500/10 border border-yellow-500/20 rounded text-yellow-300 text-xs text-center'
|
||||
}, invoice.warning),
|
||||
React.createElement('div', {
|
||||
key: 'demo-preimage',
|
||||
className: 'p-3 bg-gray-800/50 rounded border border-gray-600 text-xs font-mono text-gray-300'
|
||||
}, [
|
||||
React.createElement('div', { key: 'preimage-label', className: 'text-gray-400 mb-1' }, 'Demo Preimage:'),
|
||||
React.createElement('div', { key: 'preimage-value', className: 'break-all' },
|
||||
invoice?.preimage || 'Generating...')
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'demo-btn',
|
||||
onClick: handleDemoSession,
|
||||
disabled: isProcessing,
|
||||
className: 'w-full bg-green-600 hover:bg-green-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'demo-icon',
|
||||
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-play'} mr-2`
|
||||
}),
|
||||
isProcessing ? 'Activating...' : 'Activate Demo Session'
|
||||
])
|
||||
]),
|
||||
|
||||
paymentStatus === 'creating' && React.createElement('div', {
|
||||
key: 'creating',
|
||||
className: 'text-center p-4'
|
||||
}, [
|
||||
React.createElement('i', { className: 'fas fa-spinner fa-spin text-orange-400 text-2xl mb-2' }),
|
||||
React.createElement('div', { className: 'text-primary' }, 'Creating Lightning invoice...'),
|
||||
React.createElement('div', { className: 'text-secondary text-sm mt-1' }, 'Connecting to the Lightning Network...')
|
||||
]),
|
||||
|
||||
(paymentStatus === 'created' || paymentStatus === 'paying') && invoice && React.createElement('div', {
|
||||
key: 'payment-methods',
|
||||
className: 'space-y-6'
|
||||
}, [
|
||||
qrCodeUrl && React.createElement('div', {
|
||||
key: 'qr-section',
|
||||
className: 'text-center'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'qr-container',
|
||||
className: 'bg-white p-4 rounded-lg inline-block'
|
||||
}, [
|
||||
React.createElement('img', {
|
||||
key: 'qr-img',
|
||||
src: qrCodeUrl,
|
||||
alt: 'Lightning Payment QR Code',
|
||||
className: 'w-48 h-48'
|
||||
})
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'qr-hint',
|
||||
className: 'text-xs text-gray-400 mt-2'
|
||||
}, 'Scan with any Lightning wallet')
|
||||
]),
|
||||
|
||||
invoice.paymentRequest && React.createElement('div', {
|
||||
key: 'payment-request',
|
||||
className: 'space-y-2'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'pr-label',
|
||||
className: 'text-sm font-medium text-primary'
|
||||
}, 'Lightning Payment Request:'),
|
||||
React.createElement('div', {
|
||||
key: 'pr-container',
|
||||
className: 'p-3 bg-gray-800/50 rounded border border-gray-600 text-xs font-mono text-gray-300 cursor-pointer hover:bg-gray-700/50 transition-colors',
|
||||
onClick: () => copyToClipboard(invoice.paymentRequest),
|
||||
title: 'Click to copy'
|
||||
}, [
|
||||
invoice.paymentRequest.substring(0, 60) + '...',
|
||||
React.createElement('i', { key: 'copy-icon', className: 'fas fa-copy ml-2 text-orange-400' })
|
||||
])
|
||||
]),
|
||||
|
||||
// WebLN Payment
|
||||
React.createElement('div', {
|
||||
key: 'webln-section',
|
||||
className: 'space-y-3'
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'webln-title',
|
||||
className: 'text-primary font-medium flex items-center'
|
||||
}, [
|
||||
React.createElement('i', { key: 'bolt-icon', className: 'fas fa-bolt text-orange-400 mr-2' }),
|
||||
'WebLN wallet (recommended)'
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'webln-info',
|
||||
className: 'text-xs text-gray-400 mb-2'
|
||||
}, 'Alby, Zeus, or other WebLN-compatible wallets'),
|
||||
React.createElement('button', {
|
||||
key: 'webln-btn',
|
||||
onClick: handleWebLNPayment,
|
||||
disabled: isProcessing || paymentStatus === 'paying',
|
||||
className: 'w-full bg-orange-600 hover:bg-orange-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'webln-icon',
|
||||
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-bolt'} mr-2`
|
||||
}),
|
||||
paymentStatus === 'paying' ? 'Processing payment...' : 'Pay via WebLN'
|
||||
])
|
||||
]),
|
||||
|
||||
// Divider
|
||||
React.createElement('div', {
|
||||
key: 'divider',
|
||||
className: 'text-center text-gray-400 text-sm'
|
||||
}, '— or —'),
|
||||
|
||||
// Manual Verification
|
||||
React.createElement('div', {
|
||||
key: 'manual-section',
|
||||
className: 'space-y-3'
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'manual-title',
|
||||
className: 'text-primary font-medium'
|
||||
}, 'Manual payment confirmation'),
|
||||
React.createElement('div', {
|
||||
key: 'manual-info',
|
||||
className: 'text-xs text-gray-400'
|
||||
}, 'Pay the invoice in any wallet and enter the preimage.:'),
|
||||
React.createElement('input', {
|
||||
key: 'preimage-input',
|
||||
type: 'text',
|
||||
value: preimageInput,
|
||||
onChange: (e) => setPreimageInput(e.target.value),
|
||||
placeholder: 'Enter the preimage (64 hex characters)...',
|
||||
className: 'w-full p-3 bg-gray-800 border border-gray-600 rounded text-white placeholder-gray-400 text-sm font-mono',
|
||||
maxLength: 64
|
||||
}),
|
||||
React.createElement('button', {
|
||||
key: 'verify-btn',
|
||||
onClick: handleManualVerification,
|
||||
disabled: isProcessing || !preimageInput.trim(),
|
||||
className: 'w-full bg-green-600 hover:bg-green-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'verify-icon',
|
||||
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-check'} mr-2`
|
||||
}),
|
||||
isProcessing ? 'Checking payment...' : 'Confirm payment'
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Success State
|
||||
paymentStatus === 'paid' && React.createElement('div', {
|
||||
key: 'success',
|
||||
className: 'text-center p-6 bg-green-500/10 border border-green-500/20 rounded-lg'
|
||||
}, [
|
||||
React.createElement('i', { key: 'success-icon', className: 'fas fa-check-circle text-green-400 text-3xl mb-3' }),
|
||||
React.createElement('div', { key: 'success-title', className: 'text-green-300 font-semibold text-lg mb-1' }, '✅ Payment confirmed!'),
|
||||
React.createElement('div', { key: 'success-text', className: 'text-green-400 text-sm' }, 'The session will be activated upon connecting to the chat.')
|
||||
]),
|
||||
|
||||
// Error State
|
||||
error && React.createElement('div', {
|
||||
key: 'error',
|
||||
className: 'p-4 bg-red-500/10 border border-red-500/20 rounded-lg'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'error-content',
|
||||
className: 'flex items-start space-x-3'
|
||||
}, [
|
||||
React.createElement('i', { key: 'error-icon', className: 'fas fa-exclamation-triangle text-red-400 mt-0.5' }),
|
||||
React.createElement('div', { key: 'error-text', className: 'flex-1' }, [
|
||||
React.createElement('div', { key: 'error-message', className: 'text-red-400 text-sm' }, error),
|
||||
(error.includes('invoice') || paymentStatus === 'failed') && React.createElement('button', {
|
||||
key: 'retry-btn',
|
||||
onClick: () => createRealInvoice(selectedType),
|
||||
className: 'mt-2 text-orange-400 hover:text-orange-300 underline text-sm'
|
||||
}, 'Create a new invoice')
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
paymentStatus !== 'paid' && React.createElement('div', {
|
||||
key: 'back-section',
|
||||
className: 'pt-4 border-t border-gray-600'
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'back-btn',
|
||||
onClick: () => setStep('select'),
|
||||
className: 'w-full bg-gray-600 hover:bg-gray-500 text-white py-2 px-4 rounded transition-colors'
|
||||
}, [
|
||||
React.createElement('i', { key: 'back-icon', className: 'fas fa-arrow-left mr-2' }),
|
||||
'Choose another session'
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.PaymentModal = PaymentModal;
|
||||
284
src/components/ui/SessionTimer.jsx
Normal file
@@ -0,0 +1,284 @@
|
||||
const SessionTimer = ({ timeLeft, sessionType, sessionManager }) => {
|
||||
const [currentTime, setCurrentTime] = React.useState(timeLeft || 0);
|
||||
const [showExpiredMessage, setShowExpiredMessage] = React.useState(false);
|
||||
const [initialized, setInitialized] = React.useState(false);
|
||||
const [connectionBroken, setConnectionBroken] = React.useState(false);
|
||||
|
||||
|
||||
const [loggedHidden, setLoggedHidden] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer initialization skipped - connection broken');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let initialTime = 0;
|
||||
|
||||
if (sessionManager?.hasActiveSession()) {
|
||||
initialTime = sessionManager.getTimeLeft();
|
||||
console.log('⏱️ SessionTimer initialized from sessionManager:', Math.floor(initialTime / 1000) + 's');
|
||||
} else if (timeLeft && timeLeft > 0) {
|
||||
initialTime = timeLeft;
|
||||
console.log('⏱️ SessionTimer initialized from props:', Math.floor(initialTime / 1000) + 's');
|
||||
}
|
||||
|
||||
setCurrentTime(initialTime);
|
||||
setInitialized(true);
|
||||
setLoggedHidden(false);
|
||||
}, [sessionManager, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer props update skipped - connection broken');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeLeft && timeLeft > 0) {
|
||||
setCurrentTime(timeLeft);
|
||||
}
|
||||
setLoggedHidden(false);
|
||||
}, [timeLeft, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ Timer interval skipped - connection broken');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentTime || currentTime <= 0 || !sessionManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (connectionBroken) {
|
||||
console.log('⏱️ Timer interval stopped - connection broken');
|
||||
setCurrentTime(0);
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionManager?.hasActiveSession()) {
|
||||
const newTime = sessionManager.getTimeLeft();
|
||||
setCurrentTime(newTime);
|
||||
|
||||
if (window.DEBUG_MODE && Math.floor(Date.now() / 30000) !== Math.floor((Date.now() - 1000) / 30000)) {
|
||||
console.log('⏱️ Timer tick:', Math.floor(newTime / 1000) + 's');
|
||||
}
|
||||
|
||||
if (newTime <= 0) {
|
||||
console.log('⏱️ Session expired!');
|
||||
setShowExpiredMessage(true);
|
||||
setTimeout(() => setShowExpiredMessage(false), 5000);
|
||||
clearInterval(interval);
|
||||
}
|
||||
} else {
|
||||
console.log('⏱️ Session inactive, stopping timer');
|
||||
setCurrentTime(0);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [initialized, currentTime, sessionManager, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleSessionTimerUpdate = (event) => {
|
||||
if (event.detail.timeLeft && event.detail.timeLeft > 0) {
|
||||
setCurrentTime(event.detail.timeLeft);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForceHeaderUpdate = (event) => {
|
||||
if (sessionManager && sessionManager.hasActiveSession()) {
|
||||
const newTime = sessionManager.getTimeLeft();
|
||||
setCurrentTime(newTime);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePeerDisconnect = (event) => {
|
||||
console.log('🔌 Peer disconnect detected in SessionTimer - stopping timer permanently');
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleNewConnection = (event) => {
|
||||
console.log('🔌 New connection detected in SessionTimer - resetting connection state');
|
||||
setConnectionBroken(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleConnectionCleaned = (event) => {
|
||||
console.log('🧹 Connection cleaned - resetting SessionTimer state');
|
||||
setConnectionBroken(false);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
document.addEventListener('session-timer-update', handleSessionTimerUpdate);
|
||||
document.addEventListener('force-header-update', handleForceHeaderUpdate);
|
||||
document.addEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.addEventListener('new-connection', handleNewConnection);
|
||||
document.addEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('session-timer-update', handleSessionTimerUpdate);
|
||||
document.removeEventListener('force-header-update', handleForceHeaderUpdate);
|
||||
document.removeEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.removeEventListener('new-connection', handleNewConnection);
|
||||
document.removeEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
};
|
||||
}, [sessionManager]);
|
||||
|
||||
if (showExpiredMessage) {
|
||||
return React.createElement('div', {
|
||||
className: 'session-timer expired flex items-center space-x-2 px-3 py-1.5 rounded-lg animate-pulse',
|
||||
style: { background: 'linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.2) 100%)' }
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-exclamation-triangle text-red-400'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'message',
|
||||
className: 'text-red-400 text-sm font-medium'
|
||||
}, 'Session Expired!')
|
||||
]);
|
||||
}
|
||||
|
||||
if (!sessionManager) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - no sessionManager');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - connection broken');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentTime || currentTime <= 0) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - no time left');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loggedHidden) {
|
||||
setLoggedHidden(false);
|
||||
}
|
||||
|
||||
const totalMinutes = Math.floor(currentTime / (60 * 1000));
|
||||
const totalSeconds = Math.floor(currentTime / 1000);
|
||||
|
||||
const isDemo = sessionType === 'demo';
|
||||
const isWarning = isDemo ? totalMinutes <= 2 : totalMinutes <= 10;
|
||||
const isCritical = isDemo ? totalSeconds <= 60 : totalMinutes <= 5;
|
||||
|
||||
const formatTime = (ms) => {
|
||||
const hours = Math.floor(ms / (60 * 60 * 1000));
|
||||
const minutes = Math.floor((ms % (60 * 60 * 1000)) / (60 * 1000));
|
||||
const seconds = Math.floor((ms % (60 * 1000)) / 1000);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getTimerStyle = () => {
|
||||
const totalDuration = sessionType === 'demo' ? 6 * 60 * 1000 : 60 * 60 * 1000;
|
||||
const timeProgress = (totalDuration - currentTime) / totalDuration;
|
||||
|
||||
let backgroundColor, textColor, iconColor, iconClass, shouldPulse;
|
||||
|
||||
if (timeProgress <= 0.33) {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.15) 100%)';
|
||||
textColor = 'text-green-400';
|
||||
iconColor = 'text-green-400';
|
||||
iconClass = 'fas fa-clock';
|
||||
shouldPulse = false;
|
||||
} else if (timeProgress <= 0.66) {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(234, 179, 8, 0.15) 0%, rgba(202, 138, 4, 0.15) 100%)';
|
||||
textColor = 'text-yellow-400';
|
||||
iconColor = 'text-yellow-400';
|
||||
iconClass = 'fas fa-clock';
|
||||
shouldPulse = false;
|
||||
} else {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(220, 38, 38, 0.15) 100%)';
|
||||
textColor = 'text-red-400';
|
||||
iconColor = 'text-red-400';
|
||||
iconClass = 'fas fa-exclamation-triangle';
|
||||
shouldPulse = true;
|
||||
}
|
||||
|
||||
return { backgroundColor, textColor, iconColor, iconClass, shouldPulse };
|
||||
};
|
||||
|
||||
const timerStyle = getTimerStyle();
|
||||
|
||||
return React.createElement('div', {
|
||||
className: `session-timer flex items-center space-x-2 px-3 py-1.5 rounded-lg transition-all duration-500 ${
|
||||
isDemo ? 'demo-session' : ''
|
||||
} ${timerStyle.shouldPulse ? 'animate-pulse' : ''}`,
|
||||
style: { background: timerStyle.backgroundColor }
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `${timerStyle.iconClass} ${timerStyle.iconColor}`
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'time',
|
||||
className: `text-sm font-mono font-semibold ${timerStyle.textColor}`
|
||||
}, formatTime(currentTime)),
|
||||
React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: 'ml-2 w-16 h-1 bg-gray-700 rounded-full overflow-hidden'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-bar',
|
||||
className: `${timerStyle.textColor.replace('text-', 'bg-')} h-full rounded-full transition-all duration-500`,
|
||||
style: {
|
||||
width: `${Math.max(0, Math.min(100, (currentTime / (sessionType === 'demo' ? 6 * 60 * 1000 : 60 * 60 * 1000)) * 100))}%`
|
||||
}
|
||||
})
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.SessionTimer = SessionTimer;
|
||||
|
||||
window.updateSessionTimer = (newTimeLeft, newSessionType) => {
|
||||
console.log('⏱️ Global timer update:', { newTimeLeft, newSessionType });
|
||||
document.dispatchEvent(new CustomEvent('session-timer-update', {
|
||||
detail: { timeLeft: newTimeLeft, sessionType: newSessionType }
|
||||
}));
|
||||
};
|
||||
|
||||
console.log('✅ SessionTimer loaded with anti-spam logging fixes');
|
||||
298
src/components/ui/SessionTypeSelector.jsx
Normal file
@@ -0,0 +1,298 @@
|
||||
const SessionTypeSelector = ({ onSelectType, onCancel, sessionManager }) => {
|
||||
const [selectedType, setSelectedType] = React.useState(null);
|
||||
const [demoInfo, setDemoInfo] = React.useState(null);
|
||||
const [refreshTimer, setRefreshTimer] = React.useState(null);
|
||||
const [lastRefresh, setLastRefresh] = React.useState(Date.now());
|
||||
|
||||
// We receive up-to-date information about demo limits
|
||||
const updateDemoInfo = React.useCallback(() => {
|
||||
if (sessionManager && sessionManager.getDemoSessionInfo) {
|
||||
try {
|
||||
const info = sessionManager.getDemoSessionInfo();
|
||||
if (window.DEBUG_MODE) {
|
||||
console.log('🔄 Demo info updated:', info);
|
||||
}
|
||||
setDemoInfo(info);
|
||||
setLastRefresh(Date.now());
|
||||
} catch (error) {
|
||||
console.error('Failed to get demo info:', error);
|
||||
}
|
||||
}
|
||||
}, [sessionManager]);
|
||||
|
||||
// Update information on load and every 10 seconds
|
||||
React.useEffect(() => {
|
||||
updateDemoInfo();
|
||||
|
||||
const interval = setInterval(updateDemoInfo, 10000);
|
||||
setRefreshTimer(interval);
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [updateDemoInfo]);
|
||||
|
||||
// Clear timer on unmount
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
}
|
||||
};
|
||||
}, [refreshTimer]);
|
||||
|
||||
const sessionTypes = [
|
||||
{
|
||||
id: 'demo',
|
||||
name: 'Demo',
|
||||
duration: '6 minutes',
|
||||
price: '0 sat',
|
||||
usd: '$0.00',
|
||||
popular: false,
|
||||
description: 'Limited testing session',
|
||||
features: ['End-to-end encryption', 'Basic features', 'No payment required']
|
||||
},
|
||||
{
|
||||
id: 'basic',
|
||||
name: 'Basic',
|
||||
duration: '1 hour',
|
||||
price: '500 sat',
|
||||
usd: '$0.20',
|
||||
features: ['End-to-end encryption', 'Full features', '1 hour duration']
|
||||
},
|
||||
{
|
||||
id: 'premium',
|
||||
name: 'Premium',
|
||||
duration: '4 hours',
|
||||
price: '1000 sat',
|
||||
usd: '$0.40',
|
||||
popular: true,
|
||||
features: ['End-to-end encryption', 'Full features', '4 hours duration', 'Priority support']
|
||||
},
|
||||
{
|
||||
id: 'extended',
|
||||
name: 'Extended',
|
||||
duration: '24 hours',
|
||||
price: '2000 sat',
|
||||
usd: '$0.80',
|
||||
features: ['End-to-end encryption', 'Full features', '24 hours duration', 'Priority support']
|
||||
}
|
||||
];
|
||||
|
||||
const handleTypeSelect = (typeId) => {
|
||||
console.log(`🎯 Selecting session type: ${typeId}`);
|
||||
|
||||
if (typeId === 'demo') {
|
||||
if (demoInfo && !demoInfo.canUseNow) {
|
||||
let message = `Demo session not available.\n\n`;
|
||||
|
||||
if (demoInfo.blockingReason === 'global_limit') {
|
||||
message += `Reason: Too many global demo sessions active (${demoInfo.globalActive}/${demoInfo.globalLimit})\n`;
|
||||
message += `Please try again in a few minutes.`;
|
||||
} else if (demoInfo.blockingReason === 'daily_limit') {
|
||||
message += `Reason: Daily limit reached (${demoInfo.used}/${demoInfo.total})\n`;
|
||||
message += `Next available: ${demoInfo.nextAvailable}`;
|
||||
} else if (demoInfo.blockingReason === 'session_cooldown') {
|
||||
message += `Reason: Cooldown between sessions\n`;
|
||||
message += `Next available: ${demoInfo.nextAvailable}`;
|
||||
} else if (demoInfo.blockingReason === 'completion_cooldown') {
|
||||
message += `Reason: Wait period after last session\n`;
|
||||
message += `Next available: ${demoInfo.nextAvailable}`;
|
||||
} else {
|
||||
message += `Next available: ${demoInfo.nextAvailable}`;
|
||||
}
|
||||
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setSelectedType(typeId);
|
||||
};
|
||||
|
||||
const formatCooldownTime = (minutes) => {
|
||||
if (minutes >= 60) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return `${hours}h ${remainingMinutes}m`;
|
||||
}
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
return React.createElement('div', { className: 'space-y-6' }, [
|
||||
React.createElement('div', { key: 'header', className: 'text-center' }, [
|
||||
React.createElement('h3', {
|
||||
key: 'title',
|
||||
className: 'text-xl font-semibold text-white mb-2'
|
||||
}, 'Choose Your Session'),
|
||||
React.createElement('p', {
|
||||
key: 'subtitle',
|
||||
className: 'text-gray-300 text-sm'
|
||||
}, 'Pay via Lightning Network or try our demo session')
|
||||
]),
|
||||
|
||||
React.createElement('div', { key: 'types', className: 'space-y-3' },
|
||||
sessionTypes.map(type => {
|
||||
const isDemo = type.id === 'demo';
|
||||
const isDisabled = isDemo && demoInfo && !demoInfo.canUseNow;
|
||||
|
||||
return React.createElement('div', {
|
||||
key: type.id,
|
||||
onClick: () => !isDisabled && handleTypeSelect(type.id),
|
||||
className: `card-minimal rounded-lg p-4 border-2 transition-all ${
|
||||
selectedType === type.id ? 'border-orange-500 bg-orange-500/10' : 'border-gray-600 hover:border-orange-400'
|
||||
} ${type.popular ? 'relative' : ''} ${
|
||||
isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
|
||||
}`
|
||||
}, [
|
||||
type.popular && React.createElement('div', {
|
||||
key: 'badge',
|
||||
className: 'absolute -top-2 right-3 bg-orange-500 text-white text-xs px-2 py-1 rounded-full'
|
||||
}, 'Popular'),
|
||||
|
||||
React.createElement('div', { key: 'content', className: 'flex items-start justify-between' }, [
|
||||
React.createElement('div', { key: 'info', className: 'flex-1' }, [
|
||||
React.createElement('div', { key: 'header', className: 'flex items-center gap-2 mb-2' }, [
|
||||
React.createElement('h4', {
|
||||
key: 'name',
|
||||
className: 'text-lg font-semibold text-white'
|
||||
}, type.name),
|
||||
isDemo && React.createElement('span', {
|
||||
key: 'demo-badge',
|
||||
className: 'text-xs bg-blue-500/20 text-blue-300 px-2 py-1 rounded-full'
|
||||
}, 'FREE')
|
||||
]),
|
||||
React.createElement('p', {
|
||||
key: 'duration',
|
||||
className: 'text-gray-300 text-sm mb-1'
|
||||
}, `Duration: ${type.duration}`),
|
||||
type.description && React.createElement('p', {
|
||||
key: 'description',
|
||||
className: 'text-xs text-gray-400 mb-2'
|
||||
}, type.description),
|
||||
|
||||
isDemo && demoInfo && React.createElement('div', {
|
||||
key: 'demo-status',
|
||||
className: 'text-xs mb-2'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'availability',
|
||||
className: demoInfo.canUseNow ? 'text-green-400' : 'text-yellow-400'
|
||||
}, demoInfo.canUseNow ?
|
||||
`✅ Available (${demoInfo.available}/${demoInfo.total} today)` :
|
||||
`⏰ Next: ${demoInfo.nextAvailable}`
|
||||
),
|
||||
demoInfo.globalActive > 0 && React.createElement('div', {
|
||||
key: 'global-status',
|
||||
className: 'text-blue-300 mt-1'
|
||||
}, `🌐 Global: ${demoInfo.globalActive}/${demoInfo.globalLimit} active`)
|
||||
]),
|
||||
|
||||
type.features && React.createElement('div', {
|
||||
key: 'features',
|
||||
className: 'text-xs text-gray-400 space-y-1'
|
||||
}, type.features.map((feature, index) =>
|
||||
React.createElement('div', {
|
||||
key: index,
|
||||
className: 'flex items-center gap-1'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'check',
|
||||
className: 'fas fa-check text-green-400 w-3'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'text'
|
||||
}, feature)
|
||||
])
|
||||
))
|
||||
]),
|
||||
React.createElement('div', { key: 'pricing', className: 'text-right' }, [
|
||||
React.createElement('div', {
|
||||
key: 'sats',
|
||||
className: `text-lg font-bold ${isDemo ? 'text-green-400' : 'text-orange-400'}`
|
||||
}, type.price),
|
||||
React.createElement('div', {
|
||||
key: 'usd',
|
||||
className: 'text-xs text-gray-400'
|
||||
}, type.usd)
|
||||
])
|
||||
])
|
||||
])
|
||||
})
|
||||
),
|
||||
|
||||
demoInfo && React.createElement('div', {
|
||||
key: 'demo-info',
|
||||
className: 'bg-gradient-to-r from-blue-900/20 to-purple-900/20 border border-blue-700/50 rounded-lg p-4'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'demo-header',
|
||||
className: 'flex items-center gap-2 text-blue-300 text-sm font-medium mb-3'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-info-circle'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'title'
|
||||
}, 'Demo Session Information')
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'demo-details',
|
||||
className: 'grid grid-cols-1 md:grid-cols-2 gap-3 text-blue-200 text-xs'
|
||||
}, [
|
||||
React.createElement('div', { key: 'limits', className: 'space-y-1' }, [
|
||||
React.createElement('div', { key: 'daily' }, `📅 Daily limit: ${demoInfo.total} sessions`),
|
||||
React.createElement('div', { key: 'duration' }, `⏱️ Duration: ${demoInfo.durationMinutes} minutes each`),
|
||||
React.createElement('div', { key: 'cooldown' }, `⏰ Cooldown: ${demoInfo.sessionCooldownMinutes} min between sessions`)
|
||||
]),
|
||||
React.createElement('div', { key: 'status', className: 'space-y-1' }, [
|
||||
React.createElement('div', { key: 'used' }, `📊 Used today: ${demoInfo.used}/${demoInfo.total}`),
|
||||
React.createElement('div', { key: 'global' }, `🌐 Global active: ${demoInfo.globalActive}/${demoInfo.globalLimit}`),
|
||||
React.createElement('div', {
|
||||
key: 'next',
|
||||
className: demoInfo.canUseNow ? 'text-green-300' : 'text-yellow-300'
|
||||
}, `🎯 Status: ${demoInfo.canUseNow ? 'Available now' : demoInfo.nextAvailable}`)
|
||||
])
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'last-updated',
|
||||
className: 'text-xs text-gray-400 mt-3 text-center'
|
||||
}, `Last updated: ${new Date(lastRefresh).toLocaleTimeString()}`)
|
||||
]),
|
||||
|
||||
React.createElement('div', { key: 'buttons', className: 'flex space-x-3' }, [
|
||||
React.createElement('button', {
|
||||
key: 'continue',
|
||||
onClick: () => {
|
||||
if (selectedType) {
|
||||
console.log(`🚀 Proceeding with session type: ${selectedType}`);
|
||||
onSelectType(selectedType);
|
||||
}
|
||||
},
|
||||
disabled: !selectedType || (selectedType === 'demo' && demoInfo && !demoInfo.canUseNow),
|
||||
className: 'flex-1 lightning-button text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-all'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: selectedType === 'demo' ? 'fas fa-play mr-2' : 'fas fa-bolt mr-2'
|
||||
}),
|
||||
selectedType === 'demo' ? 'Start Demo Session' : 'Continue to Payment'
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'cancel',
|
||||
onClick: onCancel,
|
||||
className: 'px-6 py-3 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-all'
|
||||
}, 'Cancel'),
|
||||
React.createElement('button', {
|
||||
key: 'refresh',
|
||||
onClick: updateDemoInfo,
|
||||
className: 'px-3 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-all',
|
||||
title: 'Refresh demo status'
|
||||
}, React.createElement('i', { className: 'fas fa-sync-alt' }))
|
||||
]),
|
||||
|
||||
|
||||
]);
|
||||
};
|
||||
|
||||
window.SessionTypeSelector = SessionTypeSelector;
|
||||
1901
src/crypto/EnhancedSecureCryptoUtils.js
Normal file
0
src/main.js
Normal file
3450
src/network/EnhancedSecureWebRTCManager.js
Normal file
2203
src/session/PayPerSessionManager.js
Normal file
29
src/styles/animations.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/* Smooth Message Scrolling/Appearance*/
|
||||
@keyframes messageSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Icon pulsation */
|
||||
@keyframes iconPulse {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Pulse for the timer */
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
/* Scroll of logos */
|
||||
@keyframes walletLogosScroll {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
436
src/styles/components.css
Normal file
@@ -0,0 +1,436 @@
|
||||
|
||||
.minimal-bg {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-minimal {
|
||||
background: rgb(35 36 35 / 13%);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
flex-shrink: 0;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.header-minimal .cursor-pointer:hover {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.header-minimal .cursor-pointer:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* The main chat container takes up the rest of the height. */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 64px); /* 64px - header height */
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-messages-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: rgba(42, 43, 42, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(75, 85, 99, 0.2);
|
||||
}
|
||||
|
||||
/* The message container must occupy the entire available height. */
|
||||
.chat-messages-area > div:first-child {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* For mobile devices, take into account the height of the virtual keyboard */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
height: calc(100vh - 64px);
|
||||
height: calc(100dvh - 64px); /* dvh to support dynamic height on mobile */
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix for main application container */
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.card-minimal {
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(75, 85, 99, 0.2);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.card-minimal:hover {
|
||||
border-color: rgba(249, 115, 22, 0.3);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-connected { background: #10b981; }
|
||||
.status-connecting { background: #6b7280; }
|
||||
.status-failed { background: #ef4444; }
|
||||
.status-disconnected { background: #6b7280; }
|
||||
.status-verifying { background: #9ca3af; }
|
||||
|
||||
.security-shield {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
|
||||
.verification-code {
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
color: #f1f5f9;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 1.2em;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.icon-container i {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-sm { font-size: 0.875rem; }
|
||||
.icon-md { font-size: 1rem; }
|
||||
.icon-lg { font-size: 1.125rem; }
|
||||
.icon-xl { font-size: 1.25rem; }
|
||||
.icon-2xl { font-size: 1.5rem; }
|
||||
|
||||
.step-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.icon-fallback {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.fas, .far, .fab {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Improve icon rendering */
|
||||
.fas::before, .far::before, .fab::before {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Icon loading fallback */
|
||||
.icon-loading {
|
||||
opacity: 0.7;
|
||||
animation: iconPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Fallback icons content */
|
||||
.fa-fallback .fas.fa-shield-halved::before { content: "🛡️"; }
|
||||
.fa-fallback .fas.fa-shield-alt::before { content: "🛡️"; }
|
||||
.fa-fallback .fas.fa-lock::before { content: "🔒"; }
|
||||
.fa-fallback .fas.fa-unlock-alt::before { content: "🔓"; }
|
||||
.fa-fallback .fas.fa-key::before { content: "🔑"; }
|
||||
.fa-fallback .fas.fa-fingerprint::before { content: "👆"; }
|
||||
.fa-fallback .fas.fa-exchange-alt::before { content: "🔄"; }
|
||||
.fa-fallback .fas.fa-plus::before { content: "➕"; }
|
||||
.fa-fallback .fas.fa-link::before { content: "🔗"; }
|
||||
.fa-fallback .fas.fa-paste::before { content: "📋"; }
|
||||
.fa-fallback .fas.fa-check-circle::before { content: "✅"; }
|
||||
.fa-fallback .fas.fa-cogs::before { content: "⚙️"; }
|
||||
.fa-fallback .fas.fa-rocket::before { content: "🚀"; }
|
||||
.fa-fallback .fas.fa-copy::before { content: "📄"; }
|
||||
.fa-fallback .fas.fa-check::before { content: "✓"; }
|
||||
.fa-fallback .fas.fa-times::before { content: "✗"; }
|
||||
.fa-fallback .fas.fa-exclamation-triangle::before { content: "⚠️"; }
|
||||
.fa-fallback .fas.fa-info-circle::before { content: "ℹ️"; }
|
||||
.fa-fallback .fas.fa-circle::before { content: "●"; }
|
||||
.fa-fallback .fas.fa-paper-plane::before { content: "📤"; }
|
||||
.fa-fallback .fas.fa-comments::before { content: "💬"; }
|
||||
.fa-fallback .fas.fa-signature::before { content: "✍️"; }
|
||||
.fa-fallback .fas.fa-power-off::before { content: "⏻"; }
|
||||
.fa-fallback .fas.fa-arrow-left::before { content: "←"; }
|
||||
.fa-fallback .fas.fa-chevron-down::before { content: "↓"; }
|
||||
|
||||
/* Ensure fallback icons are properly sized & use emoji font */
|
||||
.fa-fallback .fas::before {
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* Icon alignment in buttons */
|
||||
button i {
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Pay-per-session UI - Обновленный трехцветный таймер */
|
||||
.session-timer {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.session-timer:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Анимация пульсации для красной зоны */
|
||||
@keyframes timer-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.session-timer.animate-pulse {
|
||||
animation: timer-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Lightning button */
|
||||
.lightning-button {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.lightning-button:hover {
|
||||
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
border: 1px solid rgba(249, 115, 22, 0.3);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #3a3b3a 0%, #4a4b4a 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-verify {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
.btn-verify:hover {
|
||||
background: linear-gradient(135deg, #3a3b3a 0%, #4a4b4a 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Wallet logos container & per-wallet filters */
|
||||
.wallet-logos-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 64px;
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wallet-logos-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
animation: walletLogosScroll 30s linear infinite;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.wallet-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 48px;
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.wallet-logo:hover {
|
||||
border-color: rgba(249, 115, 22, 0.3);
|
||||
background: rgba(249, 115, 22, 0.1);
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.wallet-logo.bitcoin-lightning { background: transparent; padding: 4px; }
|
||||
.wallet-logo.bitcoin-lightning img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.impervious { background: transparent; padding: 4px; }
|
||||
.wallet-logo.impervious img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.strike { background: transparent; padding: 4px; }
|
||||
.wallet-logo.strike img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.lnbits { background: transparent; padding: 4px; }
|
||||
.wallet-logo.lnbits img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.lightning-labs { background: transparent; padding: 4px; }
|
||||
.wallet-logo.lightning-labs img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.atomic { background: transparent; padding: 4px; }
|
||||
.wallet-logo.atomic img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.breez { background: transparent; padding: 4px; }
|
||||
.wallet-logo.breez img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.alby { background: transparent; padding: 4px; }
|
||||
.wallet-logo.alby img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.phoenix { background: transparent; }
|
||||
.wallet-logo.blixt { background: transparent; }
|
||||
.wallet-logo.zeus { background: transparent; padding: 4px; }
|
||||
.wallet-logo.zeus img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.wos { background: transparent; padding: 4px; }
|
||||
.wallet-logo.wos img { width: 80px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.muun { background: transparent; padding: 4px; }
|
||||
.wallet-logo.muun img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
/* Pause animation on hover for logos */
|
||||
.wallet-logos-container:hover .wallet-logos-track {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.message-slide {
|
||||
animation: messageSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes iconPulse {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes messageSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes walletLogosScroll {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.7);
|
||||
}
|
||||
|
||||
.accent-orange { color: #fb923c !important; }
|
||||
.accent-green { color: #34d399 !important; }
|
||||
.accent-red { color: #f87171 !important; }
|
||||
.accent-yellow { color: #fbbf24 !important; }
|
||||
.accent-purple { color: #a78bfa !important; }
|
||||
.accent-gray { color: #9ca3af !important; }
|
||||
.accent-cyan { color: #22d3ee !important; }
|
||||
.accent-blue { color: #60a5fa !important; }
|
||||
|
||||
/* Ensure icons visible in dark backgrounds */
|
||||
.text-secondary i {
|
||||
opacity: 0.8;
|
||||
}
|
||||
100
src/styles/main.css
Normal file
@@ -0,0 +1,100 @@
|
||||
/* Basic fonts and colors */
|
||||
* {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #2A2B2A;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Basic backgrounds */
|
||||
.bg-custom-bg {
|
||||
background-color: rgb(37 38 37) !important;
|
||||
}
|
||||
|
||||
.bg-my{
|
||||
background-color: rgb(26 26 26);
|
||||
}
|
||||
|
||||
.bg-header {
|
||||
background-color: rgb(35 35 35) !important;
|
||||
}
|
||||
|
||||
.minimal-bg {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2A2B2A 100%);
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Text styles */
|
||||
.text-primary { color: #f1f5f9; }
|
||||
.text-secondary { color: #9ca3af; }
|
||||
.text-muted { color: #6b7280; }
|
||||
|
||||
/* Accent colors */
|
||||
.accent-orange { color: #fb923c; }
|
||||
.accent-green { color: #34d399; }
|
||||
.accent-red { color: #f87171; }
|
||||
.accent-yellow { color: #fbbf24; }
|
||||
.accent-purple { color: #a78bfa; }
|
||||
.accent-blue { color: #60a5fa; }
|
||||
.accent-gray { color: #9ca3af; }
|
||||
.accent-cyan { color: #22d3ee; }
|
||||
|
||||
/* Custom scroll */
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(42, 43, 42, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(75, 85, 99, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(75, 85, 99, 0.7);
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
.scroll-smooth {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Improved scrolling for messages */
|
||||
.messages-container {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Media Queries (Mobile/Tablet) */
|
||||
@media (max-width: 640px) {
|
||||
.header-minimal { padding: 0 8px; }
|
||||
|
||||
.icon-container {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.verification-code {
|
||||
font-size: 0.875rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.header-minimal .max-w-7xl {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.header-minimal button {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) and (max-width: 1024px) {
|
||||
.header-minimal .max-w-7xl {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
554
test-lnbits-integration.html
Normal file
@@ -0,0 +1,554 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LNbits Integration Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: #2A2B2A;
|
||||
color: #f1f5f9;
|
||||
padding: 20px;
|
||||
}
|
||||
.test-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: #1a1a1a;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.test-section {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.success { color: #34d399; }
|
||||
.error { color: #f87171; }
|
||||
.warning { color: #fbbf24; }
|
||||
.info { color: #60a5fa; }
|
||||
button {
|
||||
background: #fb923c;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover { background: #ea580c; }
|
||||
.log {
|
||||
background: #000;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-container">
|
||||
<h1>🔧 Тест интеграции LNbits</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>📋 Конфигурация</h3>
|
||||
<p><strong>API URL:</strong> <span id="apiUrl">https://demo.lnbits.com</span></p>
|
||||
<p><strong>API Key:</strong> <span id="apiKey">623515641d2e4ebcb1d5992d6d78419c</span></p>
|
||||
<p><strong>Wallet ID:</strong> <span id="walletId">bcd00f561c7b46b4a7b118f069e68997</span></p>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>🧪 Тесты</h3>
|
||||
<button onclick="testHealthCheck()">1. Проверка API</button>
|
||||
<button onclick="testCreateInvoice()">2. Создание инвойса</button>
|
||||
<button onclick="testPaymentStatus()">3. Проверка статуса</button>
|
||||
<button onclick="testVerification()">4. Верификация платежа</button>
|
||||
<button onclick="testRealPayment()">5. Тест реального платежа</button>
|
||||
<button onclick="testDemoMode()">6. Тест Demo режима</button>
|
||||
<button onclick="copyBOLT11()">📋 Копировать BOLT11</button>
|
||||
<button onclick="runAllTests()">🚀 Запустить все тесты</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>📊 Результаты</h3>
|
||||
<div id="results"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>📝 Логи</h3>
|
||||
<div id="logs" class="log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
let testResults = [];
|
||||
let currentInvoice = null;
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const logsDiv = document.getElementById('logs');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = type;
|
||||
logEntry.textContent = `[${timestamp}] ${message}`;
|
||||
logsDiv.appendChild(logEntry);
|
||||
logsDiv.scrollTop = logsDiv.scrollHeight;
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
|
||||
function addResult(testName, success, details = '') {
|
||||
testResults.push({ testName, success, details, timestamp: Date.now() });
|
||||
updateResults();
|
||||
}
|
||||
|
||||
function updateResults() {
|
||||
const resultsDiv = document.getElementById('results');
|
||||
const passed = testResults.filter(r => r.success).length;
|
||||
const total = testResults.length;
|
||||
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="info">✅ Пройдено: ${passed}/${total}</div>
|
||||
${testResults.map(r => `
|
||||
<div class="${r.success ? 'success' : 'error'}">
|
||||
${r.success ? '✅' : '❌'} ${r.testName}
|
||||
${r.details ? `<br><small>${r.details}</small>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
async function testHealthCheck() {
|
||||
log('🔍 Тестирование доступности API...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('https://demo.lnbits.com/api/v1/health', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Api-Key': '623515641d2e4ebcb1d5992d6d78419c'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
log('✅ API доступен', 'success');
|
||||
log(`📊 Статус: ${JSON.stringify(data)}`, 'info');
|
||||
addResult('Health Check', true, `Status: ${response.status}`);
|
||||
} else {
|
||||
log(`❌ API недоступен: ${response.status}`, 'error');
|
||||
addResult('Health Check', false, `HTTP ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ Ошибка подключения: ${error.message}`, 'error');
|
||||
addResult('Health Check', false, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testCreateInvoice() {
|
||||
log('💰 Тестирование создания инвойса...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('https://demo.lnbits.com/api/v1/payments', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Api-Key': '623515641d2e4ebcb1d5992d6d78419c',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
out: false,
|
||||
amount: 500,
|
||||
memo: 'SecureBit.chat test invoice',
|
||||
unit: 'sat',
|
||||
expiry: 300
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
currentInvoice = data;
|
||||
log('✅ Инвойс создан успешно', 'success');
|
||||
log(`📋 Payment Request: ${data.bolt11 ? data.bolt11.substring(0, 50) + '...' : 'N/A'}`, 'info');
|
||||
log(`🔑 Payment Hash: ${data.payment_hash || 'N/A'}`, 'info');
|
||||
log(`💰 Amount: ${data.amount || 'N/A'} sats`, 'info');
|
||||
log(`📋 BOLT11: ${data.bolt11 ? 'Доступен' : 'N/A'}`, 'info');
|
||||
addResult('Create Invoice', true, `Amount: ${data.amount || 'N/A'} sats`);
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
log(`❌ Ошибка создания инвойса: ${response.status}`, 'error');
|
||||
log(`📄 Ответ: ${errorText}`, 'error');
|
||||
addResult('Create Invoice', false, `HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ Ошибка: ${error.message}`, 'error');
|
||||
addResult('Create Invoice', false, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testPaymentStatus() {
|
||||
if (!currentInvoice) {
|
||||
log('⚠️ Сначала создайте инвойс', 'warning');
|
||||
addResult('Payment Status', false, 'No invoice available');
|
||||
return;
|
||||
}
|
||||
|
||||
log('🔍 Проверка статуса платежа...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://demo.lnbits.com/api/v1/payments/${currentInvoice.checking_id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Api-Key': '623515641d2e4ebcb1d5992d6d78419c',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
log('✅ Статус получен', 'success');
|
||||
log(`📊 Оплачен: ${data.paid || false}`, 'info');
|
||||
log(`💰 Сумма: ${data.details?.amount || 'N/A'} sats`, 'info');
|
||||
log(`📋 Статус: ${data.status || 'N/A'}`, 'info');
|
||||
log(`📋 BOLT11: ${data.details?.bolt11 ? 'Доступен' : 'N/A'}`, 'info');
|
||||
addResult('Payment Status', true, `Paid: ${data.paid || false}, Amount: ${data.details?.amount || 'N/A'}`);
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
log(`❌ Ошибка проверки статуса: ${response.status}`, 'error');
|
||||
addResult('Payment Status', false, `HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ Ошибка: ${error.message}`, 'error');
|
||||
addResult('Payment Status', false, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testVerification() {
|
||||
log('🔐 Тестирование верификации...', 'info');
|
||||
|
||||
// Создаем фиктивный preimage для теста
|
||||
const testPreimage = Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
||||
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
log(`🔑 Тестовый preimage: ${testPreimage}`, 'info');
|
||||
|
||||
try {
|
||||
// Криптографическая верификация
|
||||
const preimageBytes = new Uint8Array(testPreimage.match(/.{2}/g).map(byte => parseInt(byte, 16)));
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', preimageBytes);
|
||||
const computedHash = Array.from(new Uint8Array(hashBuffer))
|
||||
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
log(`🔐 Вычисленный hash: ${computedHash}`, 'info');
|
||||
log('✅ Криптографическая верификация работает', 'success');
|
||||
addResult('Cryptographic Verification', true, 'SHA-256 hash computation OK');
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ Ошибка криптографической верификации: ${error.message}`, 'error');
|
||||
addResult('Cryptographic Verification', false, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testRealPayment() {
|
||||
log('💳 Тестирование реального платежа...', 'info');
|
||||
|
||||
if (!currentInvoice) {
|
||||
log('⚠️ Сначала создайте инвойс', 'warning');
|
||||
addResult('Real Payment Test', false, 'No invoice available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Создаем фиктивный preimage для теста (в реальности это придет от кошелька)
|
||||
const testPreimage = Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
||||
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
log(`🔑 Тестовый preimage: ${testPreimage}`, 'info');
|
||||
|
||||
// Проверяем через LNbits API
|
||||
const response = await fetch(`https://demo.lnbits.com/api/v1/payments/${currentInvoice.checking_id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Api-Key': '623515641d2e4ebcb1d5992d6d78419c',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
log(`📊 Статус платежа: ${JSON.stringify(data)}`, 'info');
|
||||
|
||||
// Симулируем успешный платеж для демо
|
||||
if (data.paid) {
|
||||
log('✅ Платеж уже оплачен!', 'success');
|
||||
addResult('Real Payment Test', true, 'Payment already paid');
|
||||
} else {
|
||||
log('⏳ Платеж ожидает оплаты', 'warning');
|
||||
log('💡 Для тестирования оплатите инвойс через любой Lightning кошелек', 'info');
|
||||
addResult('Real Payment Test', true, 'Payment pending - ready for testing');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ Ошибка тестирования платежа: ${error.message}`, 'error');
|
||||
addResult('Real Payment Test', false, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testDemoMode() {
|
||||
log('🎮 Тестирование Demo режима...', 'info');
|
||||
|
||||
try {
|
||||
// Симулируем PayPerSessionManager для тестирования
|
||||
const mockSessionManager = {
|
||||
sessionPrices: {
|
||||
demo: { sats: 0, hours: 0.1, usd: 0.00 },
|
||||
basic: { sats: 500, hours: 1, usd: 0.20 },
|
||||
premium: { sats: 1000, hours: 4, usd: 0.40 },
|
||||
extended: { sats: 2000, hours: 24, usd: 0.80 }
|
||||
},
|
||||
demoSessions: new Map(),
|
||||
maxDemoSessionsPerUser: 3,
|
||||
demoCooldownPeriod: 60 * 60 * 1000,
|
||||
demoSessionCooldown: 5 * 60 * 1000,
|
||||
demoSessionMaxDuration: 6 * 60 * 1000,
|
||||
usedPreimages: new Set(),
|
||||
|
||||
generateUserFingerprint() {
|
||||
const components = [
|
||||
navigator.userAgent || '',
|
||||
navigator.language || '',
|
||||
screen.width + 'x' + screen.height,
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone || '',
|
||||
navigator.hardwareConcurrency || 0,
|
||||
navigator.deviceMemory || 0,
|
||||
navigator.platform || '',
|
||||
navigator.cookieEnabled ? '1' : '0'
|
||||
];
|
||||
|
||||
let hash = 0;
|
||||
const str = components.join('|');
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
|
||||
return Math.abs(hash).toString(36);
|
||||
},
|
||||
|
||||
checkDemoSessionLimits(userFingerprint) {
|
||||
const userData = this.demoSessions.get(userFingerprint);
|
||||
const now = Date.now();
|
||||
|
||||
if (!userData) {
|
||||
return {
|
||||
allowed: true,
|
||||
reason: 'first_demo_session',
|
||||
remaining: this.maxDemoSessionsPerUser
|
||||
};
|
||||
}
|
||||
|
||||
const activeSessions = userData.sessions.filter(session =>
|
||||
now - session.timestamp < this.demoCooldownPeriod
|
||||
);
|
||||
|
||||
if (activeSessions.length >= this.maxDemoSessionsPerUser) {
|
||||
const oldestSession = Math.min(...activeSessions.map(s => s.timestamp));
|
||||
const timeUntilNext = this.demoCooldownPeriod - (now - oldestSession);
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'demo_limit_exceeded',
|
||||
timeUntilNext: timeUntilNext,
|
||||
message: `Demo limit reached (${this.maxDemoSessionsPerUser}/day). Try again in ${Math.ceil(timeUntilNext / (60 * 1000))} minutes.`,
|
||||
remaining: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (userData.lastUsed && (now - userData.lastUsed) < this.demoSessionCooldown) {
|
||||
const timeUntilNext = this.demoSessionCooldown - (now - userData.lastUsed);
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'demo_cooldown',
|
||||
timeUntilNext: timeUntilNext,
|
||||
message: `Please wait ${Math.ceil(timeUntilNext / (60 * 1000))} minutes between demo sessions.`,
|
||||
remaining: this.maxDemoSessionsPerUser - activeSessions.length
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
reason: 'within_limits',
|
||||
remaining: this.maxDemoSessionsPerUser - activeSessions.length
|
||||
};
|
||||
},
|
||||
|
||||
createDemoSession() {
|
||||
const userFingerprint = this.generateUserFingerprint();
|
||||
const demoCheck = this.checkDemoSessionLimits(userFingerprint);
|
||||
|
||||
if (!demoCheck.allowed) {
|
||||
return {
|
||||
success: false,
|
||||
reason: demoCheck.message,
|
||||
timeUntilNext: demoCheck.timeUntilNext,
|
||||
remaining: demoCheck.remaining
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const demoPreimage = this.generateSecureDemoPreimage();
|
||||
const demoPaymentHash = 'demo_' + Array.from(crypto.getRandomValues(new Uint8Array(16)))
|
||||
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionType: 'demo',
|
||||
preimage: demoPreimage,
|
||||
paymentHash: demoPaymentHash,
|
||||
duration: this.sessionPrices.demo.hours,
|
||||
durationMinutes: Math.round(this.demoSessionMaxDuration / (60 * 1000)),
|
||||
warning: `Demo session - limited to ${Math.round(this.demoSessionMaxDuration / (60 * 1000))} minutes`,
|
||||
remaining: demoCheck.remaining - 1
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'Failed to generate demo session. Please try again.',
|
||||
remaining: demoCheck.remaining
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
generateSecureDemoPreimage() {
|
||||
const timestamp = Date.now();
|
||||
const randomBytes = crypto.getRandomValues(new Uint8Array(24));
|
||||
const timestampBytes = new Uint8Array(4);
|
||||
const versionBytes = new Uint8Array(4);
|
||||
|
||||
const timestampSeconds = Math.floor(timestamp / 1000);
|
||||
timestampBytes[0] = (timestampSeconds >>> 24) & 0xFF;
|
||||
timestampBytes[1] = (timestampSeconds >>> 16) & 0xFF;
|
||||
timestampBytes[2] = (timestampSeconds >>> 8) & 0xFF;
|
||||
timestampBytes[3] = timestampSeconds & 0xFF;
|
||||
|
||||
versionBytes[0] = 0xDE;
|
||||
versionBytes[1] = 0xE0;
|
||||
versionBytes[2] = 0x00;
|
||||
versionBytes[3] = 0x01;
|
||||
|
||||
const combined = new Uint8Array(32);
|
||||
combined.set(versionBytes, 0);
|
||||
combined.set(timestampBytes, 4);
|
||||
combined.set(randomBytes, 8);
|
||||
|
||||
return Array.from(combined).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
};
|
||||
|
||||
// Тестируем demo режим
|
||||
log('🔍 Тестирование лимитов demo сессий...', 'info');
|
||||
|
||||
const userFingerprint = mockSessionManager.generateUserFingerprint();
|
||||
log(`👤 User fingerprint: ${userFingerprint.substring(0, 8)}...`, 'info');
|
||||
|
||||
const demoCheck = mockSessionManager.checkDemoSessionLimits(userFingerprint);
|
||||
log(`📊 Demo check result: ${demoCheck.allowed ? 'Allowed' : 'Denied'}`, demoCheck.allowed ? 'success' : 'warning');
|
||||
|
||||
if (demoCheck.allowed) {
|
||||
log(`✅ Demo session available. Remaining: ${demoCheck.remaining}`, 'success');
|
||||
|
||||
// Создаем demo сессию
|
||||
const demoSession = mockSessionManager.createDemoSession();
|
||||
if (demoSession.success) {
|
||||
log('🎮 Demo session created successfully!', 'success');
|
||||
log(`⏱️ Duration: ${demoSession.durationMinutes} minutes`, 'info');
|
||||
log(`🔑 Preimage: ${demoSession.preimage.substring(0, 16)}...`, 'info');
|
||||
log(`⚠️ Warning: ${demoSession.warning}`, 'warning');
|
||||
log(`📊 Remaining: ${demoSession.remaining}`, 'info');
|
||||
|
||||
addResult('Demo Mode Test', true, `Session created: ${demoSession.durationMinutes}min, Remaining: ${demoSession.remaining}`);
|
||||
} else {
|
||||
log(`❌ Demo session creation failed: ${demoSession.reason}`, 'error');
|
||||
addResult('Demo Mode Test', false, demoSession.reason);
|
||||
}
|
||||
} else {
|
||||
log(`⏳ Demo session not available: ${demoCheck.message}`, 'warning');
|
||||
addResult('Demo Mode Test', true, `Limits working: ${demoCheck.message}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ Demo mode test failed: ${error.message}`, 'error');
|
||||
addResult('Demo Mode Test', false, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function copyBOLT11() {
|
||||
if (!currentInvoice) {
|
||||
log('⚠️ Сначала создайте инвойс', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const bolt11 = currentInvoice.bolt11;
|
||||
if (!bolt11) {
|
||||
log('❌ BOLT11 недоступен', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(bolt11).then(() => {
|
||||
log('✅ BOLT11 скопирован в буфер обмена', 'success');
|
||||
log(`📋 BOLT11: ${bolt11.substring(0, 50)}...`, 'info');
|
||||
}).catch(err => {
|
||||
log(`❌ Ошибка копирования: ${err.message}`, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
async function runAllTests() {
|
||||
log('🚀 Запуск всех тестов...', 'info');
|
||||
testResults = [];
|
||||
|
||||
await testHealthCheck();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
await testCreateInvoice();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
await testPaymentStatus();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
await testVerification();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
await testRealPayment();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
await testDemoMode();
|
||||
|
||||
log('🎉 Все тесты завершены!', 'success');
|
||||
}
|
||||
|
||||
// Экспортируем функции для использования в HTML
|
||||
window.testHealthCheck = testHealthCheck;
|
||||
window.testCreateInvoice = testCreateInvoice;
|
||||
window.testPaymentStatus = testPaymentStatus;
|
||||
window.testVerification = testVerification;
|
||||
window.testRealPayment = testRealPayment;
|
||||
window.testDemoMode = testDemoMode;
|
||||
window.copyBOLT11 = copyBOLT11;
|
||||
window.runAllTests = runAllTests;
|
||||
|
||||
// Автоматический запуск при загрузке
|
||||
log('🔧 Тест интеграции LNbits загружен', 'info');
|
||||
log('📋 Нажмите "Запустить все тесты" для проверки', 'info');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||