HyperHQ Plugin Socket.IO Connection Guide
This guide explains how to connect your plugin to HyperHQ using Socket.IO for real-time, bidirectional communication.
Table of Contents
- Overview
- Connection Architecture
- Getting Started
- Connection Flow
- Event Communication
- Data Requests
- Error Handling
- Language Examples
- Troubleshooting
Overview
HyperHQ provides a Socket.IO server that enables real-time communication between the frontend and plugins. This allows for:
- Real-time Updates: Instant notifications and data synchronization
- Bidirectional Communication: Both HyperHQ and plugins can initiate communication
- Event-Driven Architecture: React to system events as they happen
- Progress Reporting: Live progress updates for long-running operations
- Persistent Connection: Maintains connection with automatic reconnection
Connection Architecture
Default Configuration
- Server URL:
http://localhost:52789 - Namespace:
/plugin - Transport: WebSocket (with polling fallback)
- Default Port: 52789 (configurable in HyperHQ settings)
Getting Started
Step 1: Define Socket.IO Configuration in Manifest
Add Socket.IO configuration to your plugin.json:
{
"id": "your-plugin-id",
"name": "Your Plugin",
"version": "1.0.0",
"executable": "plugin.exe",
"socketio": {
"enabled": true,
"namespace": "/plugin",
"reconnect": true,
"reconnectAttempts": 10,
"reconnectDelay": 2000,
"timeout": 5000
},
"permissions": {
"networkAccess": true
}
}
Step 2: Install Socket.IO Client Library
Choose the appropriate client library for your language:
JavaScript/Node.js
npm install socket.io-client
Python
pip install python-socketio
C#/.NET
<PackageReference Include="SocketIOClient" Version="3.0.8" />
Go
go get github.com/googollee/go-socket.io
Step 3: Understand Authentication
HyperHQ uses a challenge-response authentication model for plugin security:
- HyperHQ launches your plugin and passes an authentication challenge via environment variable
- Your plugin connects to Socket.IO and sends the challenge back
- HyperHQ validates the challenge and returns a session token
- Your plugin uses the session token for all subsequent requests
Environment Variables
When HyperHQ launches your plugin, it provides these environment variables:
| Variable | Description | Example |
|---|---|---|
HYPERHQ_PLUGIN_ID | Your plugin's unique ID | "my-plugin" |
HYPERHQ_SOCKET_PORT | Socket.IO server port | "52789" |
HYPERHQ_AUTH_CHALLENGE | One-time authentication challenge | "abc123..." |
HYPERHQ_PLUGIN_NAME | Your plugin's name | "My Plugin" |
HYPERHQ_PLUGIN_VERSION | Your plugin's version | "1.0.0" |
Authentication Flow Diagram
Step 4: Connect and Authenticate
Complete connection example with authentication:
// JavaScript/Node.js
const io = require('socket.io-client');
// Read environment variables
const pluginId = process.env.HYPERHQ_PLUGIN_ID;
const socketPort = process.env.HYPERHQ_SOCKET_PORT || '52789';
const authChallenge = process.env.HYPERHQ_AUTH_CHALLENGE;
// Connect to Socket.IO server
const socket = io(`http://localhost:${socketPort}/plugin`, {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 2000
});
let sessionToken = null;
socket.on('connect', () => {
console.log('Connected to HyperHQ Socket.IO server');
// Authenticate with challenge
socket.emit('authenticate', {
pluginId: pluginId,
challenge: authChallenge
});
});
socket.on('authenticated', (response) => {
if (response.success) {
console.log('Authentication successful');
sessionToken = response.sessionToken;
// Now you can register your plugin
socket.emit('plugin:register', {
id: pluginId,
version: '1.0.0',
capabilities: ['socketio', 'gameScanning']
});
} else {
console.error('Authentication failed:', response.error);
}
});
// Include session token in all requests
socket.emit('requestData', {
method: 'getSystems',
params: {},
sessionToken: sessionToken // ✅ Required for security
});
Complete Connection Flow
Overview
The complete plugin connection lifecycle:
Authentication Details
Challenge Expiration
- Challenges are valid for 30 seconds after plugin launch
- Each challenge can only be used once
- If authentication fails, HyperHQ will reject all requests
Session Token
- Session tokens are generated after successful authentication
- Tokens are unique per plugin instance (new token on each restart)
- Tokens expire when the plugin disconnects
- Must be included in all data requests for security
Security Notes
- Never hardcode authentication tokens in your plugin
- Always read the challenge from
HYPERHQ_AUTH_CHALLENGEenvironment variable - Store the session token securely in memory (not in files)
- If authentication fails, log the error and exit gracefully
Event Communication
Standard Events
Incoming Events (HyperHQ → Plugin)
| Event | Description | Payload |
|---|---|---|
request | Method execution request | {id, method, data} |
config:update | Settings changed | {settings} |
system:event | System event occurred | {type, data} |
game:data | Game data update | {games[]} |
game:launched | Game starting | {gameId, systemId} |
game:exited | Game ended | {gameId, duration} |
ping | Connection check | {timestamp} |
Outgoing Events (Plugin → HyperHQ)
| Event | Description | Payload |
|---|---|---|
plugin:register | Register plugin | {id, version, capabilities} |
plugin:response | Method response | {id, type, data} |
plugin:progress | Progress update | {task, progress, message} |
plugin:notify | User notification | {type, message} |
plugin:error | Error occurred | {error, stack} |
plugin:status | Status change | {status, details} |
pong | Connection reply | {timestamp} |
Custom Events
You can also define custom events for your plugin:
// Emit custom event
socket.emit('my-plugin:custom:event', {
action: 'custom:action',
data: { /* your data */ }
});
// Listen for custom events
socket.on('my-plugin:response', (data) => {
// Handle response
});
Data Requests
Requesting Data from HyperHQ
Plugins can request various data from HyperHQ:
Get Systems
socket.emit('requestData', {
method: 'getSystems',
params: {},
requestId: 'get-systems-001',
sessionToken: sessionToken
});
socket.on('dataResponse', (response) => {
if (response.requestId === 'get-systems-001') {
const systems = response.data; // Array of system objects
console.log('Systems:', systems);
}
});
Get Media Folders
socket.emit('requestData', {
method: 'getMediaFolders',
params: {
systemReferenceId: 'steam',
mediaTypes: ['boxart', 'background', 'screenshot', 'logo']
},
requestId: 'get-folders-001',
sessionToken: sessionToken
});
socket.on('dataResponse', (response) => {
if (response.requestId === 'get-folders-001' && response.success) {
const folders = response.data.folders;
console.log('Media folders:', folders);
}
});
Create System
socket.emit('requestData', {
method: 'createSystem',
params: {
name: 'Steam',
description: 'Steam games library',
platform: 'PC',
referenceId: 'steam',
allowedExtensions: '.exe',
enabled: true
},
requestId: 'create-system-001',
sessionToken: sessionToken
});
socket.on('dataResponse', (response) => {
if (response.requestId === 'create-system-001') {
if (response.success) {
console.log('System created:', response.data.id);
} else {
console.error('Failed:', response.error);
}
}
});
Add Games
socket.emit('requestData', {
method: 'addGames',
params: {
systemId: 'steam',
games: [
{
name: 'Half-Life 2',
fileName: 'hl2.exe',
romPath: 'C:\\Steam\\HL2',
referenceId: 'steam_220',
developer: 'Valve',
enabled: true
}
]
},
requestId: 'add-games-001',
sessionToken: sessionToken
});
socket.on('dataResponse', (response) => {
if (response.requestId === 'add-games-001' && response.success) {
console.log('Games added successfully');
}
});
Progress Reporting
For long-running operations, send progress updates:
async function longOperation() {
const totalSteps = 100;
for (let i = 0; i <= totalSteps; i++) {
// Do work...
// Report progress
socket.emit('plugin:progress', {
task: 'scanning',
taskId: 'scan-123',
progress: i,
total: totalSteps,
message: `Processing item ${i} of ${totalSteps}`,
eta: calculateETA(i, totalSteps)
});
await sleep(100);
}
// Send completion
socket.emit('plugin:progress', {
task: 'scanning',
taskId: 'scan-123',
progress: 100,
total: 100,
message: 'Complete',
completed: true
});
}
Error Handling
Connection Errors
socket.on('connect_error', (error) => {
console.error('Connection failed:', error.message);
// Implement retry logic or fallback
});
socket.on('disconnect', (reason) => {
console.warn('Disconnected:', reason);
if (reason === 'io server disconnect') {
// Server disconnected, try reconnecting
socket.connect();
}
});
socket.on('reconnect', (attemptNumber) => {
console.log(`Reconnected after ${attemptNumber} attempts`);
// Re-register plugin
registerPlugin();
});
socket.on('reconnect_error', (error) => {
console.error('Reconnection failed:', error);
});
socket.on('reconnect_failed', () => {
console.error('Failed to reconnect after maximum attempts');
// Handle permanent disconnection or show error
});
Request Timeouts
Implement timeouts for requests:
function requestWithTimeout(event, data, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Request timeout: ${event}`));
}, timeoutMs);
socket.emit(event, data);
socket.once(`response:${event}`, (response) => {
clearTimeout(timer);
resolve(response);
});
});
}
// Usage
try {
const games = await requestWithTimeout('request:games', { system: 'all' });
console.log('Received games:', games);
} catch (error) {
console.error('Request failed:', error);
}
Language Examples
JavaScript/Node.js Complete Example
const io = require('socket.io-client');
class HyperHQPlugin {
constructor() {
this.socket = null;
this.connected = false;
this.authenticated = false;
this.sessionToken = null;
this.settings = {};
// Read environment variables
this.pluginId = process.env.HYPERHQ_PLUGIN_ID;
this.authChallenge = process.env.HYPERHQ_AUTH_CHALLENGE;
this.socketPort = process.env.HYPERHQ_SOCKET_PORT || '52789';
if (!this.pluginId || !this.authChallenge) {
console.error('Missing environment variables - plugin must be launched by HyperHQ');
process.exit(1);
}
}
async initialize(settings) {
this.settings = settings;
await this.connect();
return 'initialized';
}
async connect() {
const serverUrl = `http://localhost:${this.socketPort}`;
this.socket = io(`${serverUrl}/plugin`, {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 2000
});
this.socket.on('connect', () => {
this.connected = true;
console.log('Connected to HyperHQ Socket.IO server');
// Authenticate with challenge
this.socket.emit('authenticate', {
pluginId: this.pluginId,
challenge: this.authChallenge
});
});
this.socket.on('authenticated', (response) => {
if (response.success) {
this.authenticated = true;
this.sessionToken = response.sessionToken;
console.log('Authentication successful');
this.register();
} else {
console.error('Authentication failed:', response.error);
process.exit(1);
}
});
this.socket.on('disconnect', () => {
this.connected = false;
this.authenticated = false;
console.log('Disconnected from HyperHQ');
});
this.socket.on('error', (error) => {
console.error('Socket.IO error:', error);
});
// Handle incoming requests from HyperHQ
this.socket.on('request', async (msg) => {
const response = await this.handleRequest(msg);
this.socket.emit('plugin:response', {
id: msg.id,
type: 'response',
data: response,
sessionToken: this.sessionToken
});
});
// Handle data responses
this.socket.on('dataResponse', (response) => {
this.handleDataResponse(response);
});
// Wait for authentication
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Authentication timeout'));
}, 10000);
this.socket.once('authenticated', (response) => {
clearTimeout(timeout);
if (response.success) resolve();
else reject(new Error(response.error));
});
});
}
register() {
this.socket.emit('plugin:register', {
id: this.pluginId,
type: 'executable',
capabilities: ['socketio', 'gameScanning', 'dataSync'],
version: '1.0.0'
});
console.log('Plugin registered with HyperHQ');
}
// Helper to make authenticated data requests
async requestData(method, params) {
if (!this.authenticated) {
throw new Error('Not authenticated');
}
return new Promise((resolve, reject) => {
const requestId = `${method}-${Date.now()}`;
const timeout = setTimeout(() => {
reject(new Error(`Request timeout: ${method}`));
}, 30000);
// Store the resolver for this request
const handler = (response) => {
if (response.requestId === requestId) {
clearTimeout(timeout);
this.socket.off('dataResponse', handler);
if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.error || 'Request failed'));
}
}
};
this.socket.on('dataResponse', handler);
// Send the request
this.socket.emit('requestData', {
method,
params,
requestId,
sessionToken: this.sessionToken
});
console.log(`Data request sent: ${method} (ID: ${requestId})`);
});
}
handleDataResponse(response) {
console.log('Received data response:', response.requestId);
// Responses are handled by the promise in requestData()
}
async handleRequest(message) {
switch (message.method) {
case 'initialize':
return this.initialize(message.data);
case 'execute':
return await this.execute(message.data);
case 'test':
return this.test();
case 'shutdown':
return this.shutdown();
default:
return { error: `Unknown method: ${message.method}` };
}
}
async execute(data) {
const action = data.action || 'default';
switch (action) {
case 'sync':
return await this.syncGames();
case 'get_status':
return this.getStatus();
default:
return { error: `Unknown action: ${action}` };
}
}
async syncGames() {
try {
// Request systems from HyperHQ
const systems = await this.requestData('getSystems', {});
console.log(`Found ${systems.length} systems`);
// Get media folders for a specific system
if (systems.length > 0) {
const folders = await this.requestData('getMediaFolders', {
systemReferenceId: systems[0].referenceId,
mediaTypes: ['boxart', 'background']
});
console.log('Media folders:', folders);
}
return { success: true, systemCount: systems.length };
} catch (error) {
console.error('Sync failed:', error);
return { error: error.message };
}
}
getStatus() {
return {
connected: this.connected,
authenticated: this.authenticated,
pluginId: this.pluginId
};
}
test() {
return this.connected && this.authenticated;
}
shutdown() {
if (this.socket) {
this.socket.close();
}
return 'ok';
}
}
// Usage
const plugin = new HyperHQPlugin();
plugin.initialize({}).catch(error => {
console.error('Failed to initialize plugin:', error);
process.exit(1);
});
Python Complete Example
import socketio
import asyncio
import json
import os
import sys
class HyperHQPlugin:
def __init__(self):
self.sio = socketio.AsyncClient()
self.connected = False
self.authenticated = False
self.session_token = None
self.settings = {}
# Read environment variables
self.plugin_id = os.environ.get('HYPERHQ_PLUGIN_ID')
self.auth_challenge = os.environ.get('HYPERHQ_AUTH_CHALLENGE')
self.socket_port = os.environ.get('HYPERHQ_SOCKET_PORT', '52789')
# Setup event handlers
self.setup_handlers()
def setup_handlers(self):
@self.sio.on('connect', namespace='/plugin')
async def on_connect():
self.connected = True
print('Connected to HyperHQ Socket.IO server')
# Authenticate with challenge
await self.sio.emit('authenticate', {
'pluginId': self.plugin_id,
'challenge': self.auth_challenge
}, namespace='/plugin')
@self.sio.on('authenticated', namespace='/plugin')
async def on_authenticated(response):
if response.get('success'):
self.authenticated = True
self.session_token = response.get('sessionToken')
print('Authentication successful')
await self.register()
else:
print(f"Authentication failed: {response.get('error')}")
sys.exit(1)
@self.sio.on('disconnect', namespace='/plugin')
def on_disconnect():
self.connected = False
self.authenticated = False
print('Disconnected from HyperHQ')
@self.sio.on('request', namespace='/plugin')
async def on_request(data):
response = await self.handle_request(data)
await self.sio.emit('plugin:response', {
'id': data['id'],
'type': 'response',
'data': response,
'sessionToken': self.session_token
}, namespace='/plugin')
@self.sio.on('config:update', namespace='/plugin')
async def on_config_update(data):
self.handle_config_update(data)
@self.sio.on('game:launched', namespace='/plugin')
async def on_game_launched(data):
self.handle_game_launched(data)
async def initialize(self, settings):
self.settings = settings
await self.connect()
return 'initialized'
async def connect(self):
server_url = f"http://localhost:{self.socket_port}"
await self.sio.connect(
server_url,
namespaces=['/plugin']
)
# Wait for authentication to complete
timeout = 10 # 10 second timeout
start_time = asyncio.get_event_loop().time()
while not self.authenticated:
if asyncio.get_event_loop().time() - start_time > timeout:
raise TimeoutError('Authentication timeout')
await asyncio.sleep(0.1)
async def register(self):
await self.sio.emit('plugin:register', {
'id': self.plugin_id,
'version': '1.0.0',
'capabilities': ['socketio', 'gameScanning']
}, namespace='/plugin')
async def request_data(self, method, params):
"""Helper to make authenticated data requests"""
if not self.authenticated:
raise Exception('Not authenticated')
request_id = f"req_{asyncio.get_event_loop().time()}"
await self.sio.emit('requestData', {
'method': method,
'params': params,
'requestId': request_id,
'sessionToken': self.session_token
}, namespace='/plugin')
# Wait for response (simplified - in production use proper event handling)
# This is just an example pattern
return await self._wait_for_response(request_id)
async def handle_request(self, message):
method = message.get('method')
data = message.get('data', {})
if method == 'execute':
return await self.execute(data)
elif method == 'test':
return self.test()
else:
return {'error': f'Unknown method: {method}'}
async def execute(self, data):
action = data.get('action', 'default')
# Send progress update
await self.sio.emit('plugin:progress', {
'task': action,
'progress': 50,
'message': 'Processing...'
}, namespace='/plugin')
# Do work...
await asyncio.sleep(1)
return {'success': True, 'result': 'completed'}
def test(self):
return self.connected
Troubleshooting
Connection Issues
Plugin cannot connect to Socket.IO server
-
Check HyperHQ is running
- Verify HyperHQ is started and the Socket.IO server is active
- Check HyperHQ logs for Socket.IO server status
-
Verify server URL
- Default:
http://localhost:52789 - Check if custom port is configured in HyperHQ
- Default:
-
Firewall/Antivirus
- Add exception for localhost:52789
- Temporarily disable to test
-
Check namespace
- Must use
/pluginnamespace - Example:
io('http://localhost:52789/plugin')
- Must use
Authentication Issues
-
Authentication Failed - Invalid Challenge
- Cause: Challenge not found or already used
- Solution: Ensure you're reading
HYPERHQ_AUTH_CHALLENGEfrom environment variables - Solution: Don't cache the challenge - read it fresh on each plugin start
-
Authentication Timeout
- Cause: Plugin took too long to authenticate (>30 seconds)
- Solution: Connect and authenticate immediately after plugin starts
- Solution: Don't perform long operations before authentication
-
Session Token Expired
- Cause: Plugin disconnected and session token is no longer valid
- Solution: On reconnection, authenticate again with the original challenge
- Note: Each plugin launch gets a new challenge/session
-
Missing Environment Variables
- Check: Verify HyperHQ is passing environment variables
- Debug: Print
process.env.HYPERHQ_AUTH_CHALLENGEat startup - Common Issue: Running plugin manually without HyperHQ launching it
Connection drops frequently
-
Increase reconnection attempts
"socketio": {
"reconnectAttempts": 20,
"reconnectDelay": 3000
} -
Implement heartbeat
setInterval(() => {
if (socket.connected) {
socket.emit('heartbeat', { timestamp: Date.now() });
}
}, 30000); -
Check for memory leaks
- Remove event listeners when not needed
- Clear large data structures
Event Issues
Events not received
-
Verify event names match exactly
- Event names are case-sensitive
- Check for typos
-
Ensure proper namespace
// Correct
socket.on('request', handler);
// Wrong - no namespace needed in event name
socket.on('/plugin/request', handler); -
Check registration
- Plugin must register before receiving events
- Verify registration response
Response timeout
-
Implement proper async handling
socket.on('request', async (msg) => {
try {
const result = await processRequest(msg);
socket.emit('plugin:response', { id: msg.id, data: result });
} catch (error) {
socket.emit('plugin:error', { id: msg.id, error: error.message });
}
}); -
Send progress for long operations
// Prevent timeout by sending progress
const interval = setInterval(() => {
socket.emit('plugin:progress', { task: 'processing', progress: count });
}, 1000);
Performance Issues
High CPU usage
-
Throttle event emissions
const throttle = (func, delay) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
};
const throttledProgress = throttle((progress) => {
socket.emit('plugin:progress', progress);
}, 100); -
Use binary for large data
// Send large data as binary
const buffer = Buffer.from(largeData);
socket.binary(true).emit('plugin:binary-data', buffer);
Best Practices
- Always implement reconnection logic
- Send regular progress updates for long operations
- Handle all error cases gracefully
- Clean up resources on disconnect
- Use structured logging for debugging
- Implement request timeouts
- Validate all incoming data
- Use TypeScript/type hints for better IDE support
Security Best Practices
Authentication Security
-
Never Hardcode Credentials
// ❌ WRONG - Don't hardcode tokens
const authChallenge = "abc123hardcoded";
// ✅ CORRECT - Always read from environment
const authChallenge = process.env.HYPERHQ_AUTH_CHALLENGE; -
Validate Environment Variables
// Check that required variables are present
if (!process.env.HYPERHQ_AUTH_CHALLENGE) {
console.error('Missing HYPERHQ_AUTH_CHALLENGE - plugin must be launched by HyperHQ');
process.exit(1);
} -
Store Session Token Securely
- Keep session token in memory only (never write to disk)
- Don't log the session token in plain text
- Clear token on disconnect
-
Handle Authentication Failures
socket.on('authenticated', (response) => {
if (!response.success) {
console.error('Authentication failed:', response.error);
// Exit gracefully - don't continue without authentication
process.exit(1);
}
}); -
Include Session Token in All Requests
// ✅ Always include session token for security
socket.emit('requestData', {
method: 'getSystems',
params: {},
sessionToken: this.sessionToken // Required!
});
// ❌ WRONG - Requests without token will be rejected
socket.emit('requestData', {
method: 'getSystems',
params: {}
});
Data Security
-
Validate All Incoming Data
socket.on('request', (message) => {
// Validate message structure
if (!message.id || !message.method) {
console.error('Invalid message format');
return;
}
// Validate method is expected
const allowedMethods = ['initialize', 'execute', 'test', 'shutdown'];
if (!allowedMethods.includes(message.method)) {
console.error('Unknown method:', message.method);
return;
}
}); -
Sanitize User Input
- Never directly execute user-provided code
- Validate file paths before accessing files
- Use parameterized queries for databases
-
Respect Permissions
- Only access resources specified in your plugin manifest
- Don't attempt to bypass permission checks
- Request minimal permissions needed
Network Security
-
Use Localhost Only
- Socket.IO server runs on
localhost- never connect to external hosts - HyperHQ provides the port via
HYPERHQ_SOCKET_PORT
- Socket.IO server runs on
-
Timeout All Requests
const timeout = setTimeout(() => {
reject(new Error('Request timeout'));
}, 30000); -
Handle Connection Errors
- Implement proper error handling for network failures
- Don't expose sensitive information in error messages
Next Steps
- Review the Plugin Developer Guide for complete plugin development information
- Check the API Reference for all available methods
- See template examples in
templates/for working implementations - Join the HyperHQ Discord for community support
With Socket.IO, your plugin can fully integrate with HyperHQ's real-time ecosystem! 🚀