Architecture
Technical Implementation
hacka.re is built as a pure client-side application using vanilla JavaScript, HTML, and CSS. This approach eliminates the need for a backend server aside from the necessary OpenAI-compatible API endpoint for GenAI model access.
Tool Calling Implementation Demo
Below is a demonstration of the tool calling implementation in action (2x speed):
The application communicates directly with your configured provider using your API key, which is stored in your browser's localStorage. All message processing, including markdown rendering and syntax highlighting, happens locally in your browser.
Few dependencies limits the attack surface and thus increases the resilince to various attacks. We only depend on `marked` for rendering markdown, `dompurify` to prevent cross-site scripting, `tweetnacl` for mininmal-complexity strong in-browser encryption, and `qrcode` to make qr codes out of these self-contained GPTs links. Everything else including all of the UI components and logic have been 99%+ vibe-coded using the Claude 3.7 Sonnet model. hacka.re is by design pretty bare-bones but allows for arbitrary expansion of purpose-specific features through further LLM-assisted development with limited time and effort investments.
// Example of the streaming implementation const response = await fetch(this.chatEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` }, body: JSON.stringify({ model: this.currentModel, messages: apiMessages, stream: true }) }); // Process the stream const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let aiResponse = ''; while (true) { const { done, value } = await reader.read(); if (done) break; // Decode chunk and update UI in real-time const chunk = decoder.decode(value); // Process SSE format... }
Modular Storage Architecture
hacka.re implements a modular storage architecture that separates concerns and improves maintainability. The storage system is divided into several specialized components:
- EncryptionService: Handles all encryption and decryption operations
- NamespaceService: Manages namespaces for storage isolation based on title/subtitle
- CoreStorageService: Provides the core storage operations with encryption support
- DataService: Implements specific data type operations (API keys, models, chat history, etc.)
- StorageService: Main entry point that coordinates all storage operations
This modular approach offers several benefits:
- Improved code organization with clear separation of concerns
- Better testability of individual components
- Easier maintenance and future enhancements
- More flexible encryption and namespace management
// Example of the modular storage architecture // EncryptionService handles encryption/decryption window.EncryptionService = (function() { // Encryption-related functions function encrypt(data, passphrase) { // Implementation of encryption } function decrypt(encryptedData, passphrase) { // Implementation of decryption } // Public API return { encrypt: encrypt, decrypt: decrypt, // Other encryption-related methods }; })(); // NamespaceService manages storage namespaces window.NamespaceService = (function() { // Namespace management functions function getNamespace() { // Get current namespace based on title/subtitle } function getNamespacedKey(baseKey) { // Get namespaced key for storage } // Public API return { getNamespace: getNamespace, getNamespacedKey: getNamespacedKey, // Other namespace-related methods }; })(); // CoreStorageService provides basic storage operations window.CoreStorageService = (function() { // Core storage functions function setValue(baseKey, value) { // Set value with optional encryption } function getValue(baseKey, legacyKey, migrateFn) { // Get value with optional decryption } // Public API return { setValue: setValue, getValue: getValue, // Other storage-related methods }; })(); // DataService implements specific data operations window.DataService = (function() { // Data-specific functions function saveApiKey(apiKey) { CoreStorageService.setValue(STORAGE_KEYS.API_KEY, apiKey); } function getApiKey() { return CoreStorageService.getValue( STORAGE_KEYS.API_KEY, STORAGE_KEYS.API_KEY, saveApiKey ); } // Public API return { saveApiKey: saveApiKey, getApiKey: getApiKey, // Other data-specific methods }; })(); // StorageService is the main entry point window.StorageService = (function() { // Initialize the core storage service CoreStorageService.init(); // Public API - expose all data service methods return { // Namespace methods getNamespace: NamespaceService.getNamespace, getNamespacedKey: NamespaceService.getNamespacedKey, // Data methods saveApiKey: DataService.saveApiKey, getApiKey: DataService.getApiKey, // Other methods }; })();
Comprehensive Secure Sharing - Technical Details
hacka.re includes a feature to securely share various aspects of your configuration with others through session key-protected URL-based sharing. Note that links created with the share feature are susceptible to brute force attacks and rely entirely on a strong password- or session key to be resilient. The 12 random alphanumerical characters produced by default should be strong enough for most real-world applications but could of course be made arbitrarily stronger simply by increasing the number of rounds used for key derivation at the expense of compute and application responsiveness.
Comprehensive sharing options:
- API Key: Share your API key for access to models
- System Prompt: Share your custom system prompt for consistent AI behavior
- Active Model: Share your selected model preference with automatic fallback if unavailable
- Conversation Data: Share recent conversation history with configurable message count
How it works:
- When you create a shareable link, you select what to include (base URL, API key, model, system prompt, conversation data)
- A real-time link length indicator shows the estimated size of the generated link
- You can generate a strong random session key or provide your own
- Your selected data is encrypted using a key derived from your session key with cryptographically sound methods
- Only the encrypted data is included in the URL after the # symbol (the encryption key is NOT included)
- When someone opens the link, they're prompted to enter the session key/password
- The application derives the decryption key from the entered session key/password and attempts to decrypt the data
- If successful, the data is applied to their session and the URL is cleared from the browser history
- For model preferences, the system verifies availability with the recipient's API key and falls back gracefully if needed
- If unsuccessful (wrong session key/password), they're prompted to try again
Team collaboration with session keys:
- Teams can agree on a common session key to use for sharing links
- Each team member can enter and lock this session key in their sharing settings
- When a team member receives a link created with the team's session key, the system automatically tries the locked session key first
- If the session key works, the shared data is applied without prompting for the session key
- This allows seamless sharing among team members without repeatedly entering the same session key
- The session key should be shared through a secure channel separate from the links themselves
Technical implementation: The feature uses TweetNaCl.js (a JavaScript port of the NaCl crypto library) for the encryption, combined with session key-based key derivation:
// Simplified version of the comprehensive sharing implementation // Generate a strong random alphanumeric session key function generateStrongSessionKey() { const length = 12; // Fixed length of 12 characters const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let password = ""; // Get cryptographically strong random values const randomValues = new Uint8Array(length); window.crypto.getRandomValues(randomValues); // Convert to session key characters for (let i = 0; i < length; i++) { password += charset[randomValues[i] % charset.length]; } return password; } // Create a comprehensive shareable link with selected data function createComprehensiveShareableLink(options, sessionKey) { // Create a payload object with only the selected data const payload = {}; // Include API key if selected if (options.includeApiKey && options.apiKey) { payload.apiKey = options.apiKey; } // Include system prompt if selected if (options.includeSystemPrompt && options.systemPrompt) { payload.systemPrompt = options.systemPrompt; } // Include active model if selected if (options.includeModel && options.model) { payload.model = options.model; } // Include conversation data if selected if (options.includeConversation && options.messages && options.messages.length > 0) { // Include only the specified number of most recent messages const messageCount = options.messageCount || 1; const startIndex = Math.max(0, options.messages.length - messageCount); payload.messages = options.messages.slice(startIndex); } // Encrypt the payload using the session key const encryptedData = encryptData(payload, sessionKey); // Create URL with only the encrypted data (no key) return `${window.location.href.split('#')[0]}#shared=${encryptedData}`; } // Encrypt data with a session key function encryptData(payloadObj, sessionKey) { // Convert to JSON string const jsonString = JSON.stringify(payloadObj); const plain = nacl.util.decodeUTF8(jsonString); // Generate salt and derive key const salt = nacl.randomBytes(SALT_LENGTH); const key = deriveSeed(sessionKey, salt); // Generate nonce for secretbox const nonce = nacl.randomBytes(nacl.secretbox.nonceLength); // Encrypt with secretbox (symmetric encryption) const cipher = nacl.secretbox(plain, nonce, key); // Combine salt, nonce, and cipher const fullMessage = new Uint8Array( salt.length + nonce.length + cipher.length ); let offset = 0; fullMessage.set(salt, offset); offset += salt.length; fullMessage.set(nonce, offset); offset += nonce.length; fullMessage.set(cipher, offset); // Convert to base64 for URL-friendly format return nacl.util.encodeBase64(fullMessage); } // Extract and decrypt shared data from the URL function extractSharedData(sessionKey) { try { // Get the hash fragment const hash = window.location.hash; // Check if it contains shared data if (hash.includes('#shared=')) { // Extract the encrypted data const encryptedData = hash.split('#shared=')[1]; if (!encryptedData) { return null; } // Decrypt the data const data = decryptData(encryptedData, sessionKey); if (!data) { return null; } // Return all decrypted data return { apiKey: data.apiKey, systemPrompt: data.systemPrompt || null, model: data.model || null, messages: data.messages || null }; } return null; } catch (error) { console.error('Error extracting shared data:', error); return null; } }
Security considerations:
- True session key-based encryption: The encryption key is derived from the session key and is never included in the URL
- Salt-based key derivation: A unique salt is generated for each link, protecting against rainbow table attacks
- Multiple hashing iterations: The key derivation process uses multiple iterations to increase security
- URL fragment (#) usage: The data after # is not sent to servers when requesting a page, providing protection against server logging
- Session key confirmation: When creating a link, you must confirm your session key to prevent typos
- Intended for trusted sharing: Still only share these links with people you trust, as they will have full access to your API provider account once they enter the correct session key
- Temporary usage: Consider revoking your API key after sharing if you're concerned about unauthorized access
When to use each sharing option:
- API Key: Essential for giving someone access to your API provider account
- System Prompt: Share your custom instructions to ensure consistent AI behavior
- Active Model: Share your preferred model selection for consistent results
- Conversation Data: Share context from your current conversation to continue a discussion
QR code generation:
- After generating a shareable link, a QR code is automatically created for easy mobile sharing
- The QR code encodes the complete shareable link including the encrypted data
- The system monitors the link length and provides warnings when approaching QR code capacity limits
- Standard QR codes can typically handle up to 1500-2000 bytes of data
- When links exceed this limit (common with large system prompts or conversation history), a warning is displayed
- The QR code uses error correction level L (low) to maximize data capacity
- Recipients can scan the QR code with any standard QR code scanner to open the link
- They will still need the session key to decrypt the data