Authentication
Data Stream uses API Key authentication to protect endpoints and enable rate limiting per client.
# Create your first API key
task apikey:create -- -name "My App" -admin
# Use in requests
curl -H "X-API-Key: sk_live_xxxxx" https://datastream.hypetech.games/api/games
Overview
| Feature | Description |
|---|---|
| Key Types | Private (sk_*), Public (pk_*), Browser-bound |
| Token Types | Short-lived tokens (tok_*) with 15s TTL |
| Rate Limiting | Configurable per key (default: 1000 req/min) |
| Browser Security | FingerprintJS + Origin validation + auto-refresh |
Token Types
Data Stream uses three types of credentials:
| Prefix | Type | TTL | Use Case |
|---|---|---|---|
sk_live_* | Private Key | Permanent | Server-side only |
pk_live_* | Public Key | Permanent | Browser with Origin validation |
tok_* | Short-lived Token | 15 seconds | REST/WebSocket browser sessions |
Private Keys (sk_live_*)
Private keys are for server-side use only. Never expose in browser code.
sk_live_24c43125d90785dc51d50494b8f61e4c670c1913d4e89ce9f4d5deac30aa4735
Characteristics:
- 64 character hex string after prefix
- No Origin restrictions
- Higher rate limit (1000 req/min default)
- Full scope access when admin
Use for:
- Backend services
- Server-to-server communication
- Admin operations
- CLI tools
Public Keys (pk_live_*)
Public keys can be embedded in browser applications with Origin validation.
pk_live_a1b2c3d4e5f6789012345678901234567890123456789012345678901234
Characteristics:
- Requires
allowed_originsconfiguration - Lower rate limit (100 req/min default)
- Origin header validated on every request
- Can be combined with browser fingerprint for extra security
Use for:
- Browser dashboards
- Client-side applications
- Demo pages
Short-lived Tokens (tok_*)
Tokens are temporary credentials for browser sessions.
tok_1b0f96fcb66fce576aeb015f55097323
Characteristics:
- 15 second TTL (prevents curl replay attacks)
- Generated from
pk_*orsk_*keys - Inherits origin restrictions from parent key
- Used for REST API and WebSocket connections
Tokens expire in 15 seconds. Implement auto-refresh on 401 responses.
Browser Authentication Flow
For browser applications, Data Stream provides a secure authentication flow using FingerprintJS for device identification.
How It Works
Implementation
// 1. Initialize FingerprintJS
const fpPromise = import('https://openfpcdn.io/fingerprintjs/v4')
.then(FingerprintJS => FingerprintJS.load());
// 2. Get visitor ID
async function getVisitorId() {
const fp = await fpPromise;
const result = await fp.get();
return result.visitorId;
}
// 3. Get browser-bound key
async function getBrowserKey(visitorId) {
const res = await fetch('/api/auth/browser-key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
visitor_id: visitorId,
bound_domain: window.location.hostname
})
});
const data = await res.json();
return data.key; // pk_live_*
}
// 4. Get short-lived token
async function getToken(apiKey) {
const res = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'X-API-Key': apiKey }
});
const data = await res.json();
return data.token; // tok_*
}
// 5. Auto-refresh on 401
async function authFetch(url, options = {}) {
options.headers = { ...options.headers, 'X-API-Key': state.token };
let res = await fetch(url, options);
if (res.status === 401) {
console.log('Token expired, refreshing...');
state.token = await getToken(state.apiKey);
options.headers['X-API-Key'] = state.token;
res = await fetch(url, options);
}
return res;
}
// Complete initialization
async function initBrowserAuth() {
const visitorId = await getVisitorId();
const apiKey = await getBrowserKey(visitorId);
const token = await getToken(apiKey);
return { visitorId, apiKey, token };
}
Security Features
| Feature | Description |
|---|---|
| Browser Fingerprint | Device identification via FingerprintJS |
| Origin Binding | Token bound to requesting Origin header |
| Domain Binding | Key tied to specific domain |
| Short TTL | 15s token lifetime prevents replay attacks |
| Auto-refresh | Transparent token renewal on 401 |
Why 15 Second TTL?
The short token TTL provides several security benefits:
- Prevents curl replay: Tokens copied from DevTools expire before they can be used
- Limits exposure: Stolen tokens are only valid briefly
- Browser-friendly: Auto-refresh is transparent to the user
- Origin bound: Tokens validate the requesting origin
# This will fail - token expires in 15s
curl -H "X-API-Key: tok_xxxxx" https://api/games
# Error: Token expired
# Browser auto-refreshes seamlessly
fetch('/api/games'); // Works via authFetch()
API Key Types
Private Keys (Server-Side)
Private keys start with sk_live_ and should never be exposed to browsers.
# Create private key
task apikey:create -- -name "Backend Service" -scopes "read:rounds,stream:*"
Public Keys (Browser)
Public keys start with pk_live_ and can be used in browser applications with origin restrictions.
# Create public key with origin restriction
task apikey:create -- -name "Dashboard" -public -origins "https://dashboard.example.com"
Public keys require origin validation. Configure allowed origins when creating the key.
Creating API Keys
Using Taskfile
# Create admin key (all permissions)
task apikey:create -- -name "Admin Key" -admin
# Create limited key with specific scopes
task apikey:create -- -name "Mobile App" -scopes "read:rounds,stream:game" -rate-limit 500
# Create public key for browser dashboards
task apikey:create -- -name "Dashboard" -public -origins "https://app.example.com"
# List all keys
task apikey:list
# Revoke a key
task apikey:revoke -- -id <key-id>
# Delete a key
task apikey:delete -- -id <key-id>
Using REST API (Admin Only)
# Create key via API (requires admin key)
curl -X POST https://datastream.hypetech.games/api/auth/keys \
-H "X-API-Key: sk_live_admin_key" \
-H "Content-Type: application/json" \
-d '{
"name": "New App Key",
"scopes": ["read:rounds", "stream:game"],
"rate_limit": 500
}'
# List keys
curl -H "X-API-Key: sk_live_admin_key" \
https://datastream.hypetech.games/api/auth/keys
Available Scopes
| Scope | Description | Endpoints |
|---|---|---|
read:rounds | Read round data | /api/latest/*, /api/history/* |
read:games | Read game list | /api/games |
stream:* | Stream all protocols | /ws/*, /sse/*, gRPC streams |
stream:game | Stream specific game | /ws/:game, /sse/:game |
admin | Full admin access | /api/auth/keys/* |
Using API Keys
REST API
Include the key in request headers:
# Using X-API-Key header (recommended)
curl -H "X-API-Key: sk_live_xxxxx" \
https://datastream.hypetech.games/api/games
# Using Authorization header
curl -H "Authorization: Bearer sk_live_xxxxx" \
https://datastream.hypetech.games/api/latest/crash
WebSocket
WebSocket requires a short-lived token (15 seconds) obtained via REST API:
// Step 1: Get token using API key
const tokenResponse = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'X-API-Key': 'pk_live_xxxxx' }
});
const { token } = await tokenResponse.json();
// Step 2: Connect with token via Sec-WebSocket-Protocol
const ws = new WebSocket('wss://datastream.hypetech.games/ws/crash', [token]);
ws.onmessage = (e) => {
console.log(JSON.parse(e.data));
};
// Step 3: Reconnect on close with new token
ws.onclose = async () => {
const newToken = await getToken();
const newWs = new WebSocket('wss://datastream.hypetech.games/ws/crash', [newToken]);
};
Token format: tok_xxxxxxxxxxxxxxxxxxxxxxxx
Tokens expire in 15 seconds. Get a new token for each connection or reconnection.
Server-Sent Events (SSE)
SSE uses a token in the query parameter (headers not supported by EventSource):
// Get token first
const token = await getToken();
// Connect with token
const sse = new EventSource(`/sse/crash?token=${encodeURIComponent(token)}`);
sse.addEventListener('message', (e) => {
console.log(JSON.parse(e.data));
});
// Reconnect on error with new token
sse.onerror = async () => {
sse.close();
const newToken = await getToken();
// Reconnect with new token...
};
gRPC
Include the key in metadata:
# Using grpcurl
grpcurl -plaintext \
-H "x-api-key: sk_live_xxxxx" \
datastream.hypetech.games:50051 \
datastream.DataStream/ListGames
# Or using authorization header
grpcurl -plaintext \
-H "authorization: Bearer sk_live_xxxxx" \
-d '{"game_slug":"crash"}' \
datastream.hypetech.games:50051 \
datastream.DataStream/StreamByGame
Go client:
import "google.golang.org/grpc/metadata"
// Create context with API key
ctx := metadata.AppendToOutgoingContext(context.Background(),
"x-api-key", "sk_live_xxxxx")
// Make call with authenticated context
stream, err := client.StreamByGame(ctx, &pb.StreamRequest{
GameSlug: "crash",
})
Auth Endpoints
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/auth/scopes | GET | Public | List available scopes |
/api/auth/token | POST | Required | Get short-lived token (15s) |
/api/auth/browser-key | POST | Public | Get browser-bound key |
/api/auth/keys | GET | Admin | List all API keys |
/api/auth/keys | POST | Admin | Create new API key |
/api/auth/keys/:id/revoke | POST | Admin | Revoke an API key |
/api/auth/keys/:id | DELETE | Admin | Delete an API key |
Get Browser Key
curl -X POST https://datastream.hypetech.games/api/auth/browser-key \
-H "Content-Type: application/json" \
-d '{
"visitor_id": "abc123fingerprint",
"bound_domain": "dashboard.example.com"
}'
Response:
{
"success": true,
"key": "pk_live_a1b2c3d4...",
"expires_at": 0,
"new": true
}
Get Token
curl -X POST https://datastream.hypetech.games/api/auth/token \
-H "X-API-Key: pk_live_xxxxx"
Response:
{
"success": true,
"token": "tok_1b0f96fcb66fce576aeb015f55097323",
"expires_in": 15,
"usage": "Use this token for REST API (X-API-Key header) or WebSocket (Sec-WebSocket-Protocol header)"
}
List Scopes
curl https://datastream.hypetech.games/api/auth/scopes
Response:
{
"scopes": [
{"name": "read:rounds", "description": "Read round data"},
{"name": "read:games", "description": "Read game list"},
{"name": "stream:*", "description": "Stream all data"},
{"name": "stream:game", "description": "Stream specific game"},
{"name": "admin", "description": "Admin access"}
]
}
Rate Limiting
Each API key has a configurable rate limit:
| Key Type | Default Rate Limit |
|---|---|
Private (sk_*) | 1000 req/min |
Public (pk_*) | 100 req/min |
Rate limit headers:
X-Ratelimit-Limit: 1000
X-Ratelimit-Remaining: 997
X-Ratelimit-Reset: 45
Rate limit exceeded (HTTP 429):
{
"error": "Too many requests, please slow down",
"success": false
}
Environment Variables
| Variable | Default | Description |
|---|---|---|
AUTH_REQUIRE_API | false | Require API key for REST endpoints |
AUTH_REQUIRE_WS | false | Require token for WebSocket |
AUTH_REQUIRE_SSE | false | Require API key for SSE |
AUTH_REQUIRE_GRPC | true | Require auth for gRPC |
Complete Browser Example
class DataStreamBrowserClient {
constructor() {
this.apiKey = null;
this.token = null;
this.visitorId = null;
}
// Initialize with FingerprintJS
async init() {
// Load FingerprintJS
const FingerprintJS = await import('https://openfpcdn.io/fingerprintjs/v4')
.then(m => m.load());
const fp = await FingerprintJS.get();
this.visitorId = fp.visitorId;
// Get browser-bound key
const keyRes = await fetch('/api/auth/browser-key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
visitor_id: this.visitorId,
bound_domain: window.location.hostname
})
});
const keyData = await keyRes.json();
this.apiKey = keyData.key;
// Get initial token
await this.refreshToken();
return this;
}
// Refresh token
async refreshToken() {
const res = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'X-API-Key': this.apiKey }
});
const data = await res.json();
this.token = data.token;
return this.token;
}
// Authenticated fetch with auto-refresh
async fetch(url, options = {}) {
options.headers = { ...options.headers, 'X-API-Key': this.token };
let res = await fetch(url, options);
if (res.status === 401) {
await this.refreshToken();
options.headers['X-API-Key'] = this.token;
res = await fetch(url, options);
}
return res;
}
// Connect WebSocket
async connectWebSocket(game) {
const token = await this.refreshToken();
const ws = new WebSocket(
`wss://${window.location.host}/ws/${game}`,
[token]
);
ws.onclose = () => {
setTimeout(() => this.connectWebSocket(game), 1000);
};
return ws;
}
// Connect SSE
async connectSSE(game) {
const token = await this.refreshToken();
return new EventSource(`/sse/${game}?token=${encodeURIComponent(token)}`);
}
}
// Usage
const client = new DataStreamBrowserClient();
await client.init();
// REST with auto-refresh
const games = await client.fetch('/api/games').then(r => r.json());
// WebSocket with auto-reconnect
const ws = await client.connectWebSocket('crash');
ws.onmessage = (e) => console.log(JSON.parse(e.data));
// SSE
const sse = await client.connectSSE('crash');
sse.onmessage = (e) => console.log(JSON.parse(e.data));
Security Best Practices
- Never expose private keys - Use public keys (
pk_*) for browser apps - Use browser authentication - FingerprintJS + short-lived tokens
- Implement auto-refresh - Handle 401 responses transparently
- Rotate keys regularly - Revoke and recreate keys periodically
- Use minimal scopes - Only grant necessary permissions
- Monitor usage - Check
last_usedtimestamp for suspicious activity - Set appropriate rate limits - Protect against abuse
- Use HTTPS - Always use TLS in production