keeperboard
v2.2.2
Published
TypeScript client SDK for KeeperBoard leaderboard-as-a-service
Maintainers
Readme
KeeperBoard SDK
TypeScript client for KeeperBoard leaderboard-as-a-service. Works in browsers and Node.js.
Installation
npm install keeperboardQuick Start (15 lines)
import { KeeperBoardSession } from 'keeperboard';
const session = new KeeperBoardSession({
apiKey: 'kb_dev_your_api_key',
leaderboard: 'main',
cache: { ttlMs: 30000 }, // Optional: 30s cache
retry: { maxAgeMs: 86400000 }, // Optional: 24h retry queue
});
// Submit a score
const result = await session.submitScore(1500);
if (result.success) {
console.log(`Rank #${result.rank}, New high: ${result.isNewHighScore}`);
}
// Get leaderboard with player's rank
const snapshot = await session.getSnapshot({ limit: 10 });
snapshot.entries.forEach(e => {
console.log(`#${e.rank} ${e.playerName}: ${e.score}`, e.isCurrentPlayer ? '(you)' : '');
});Two API Layers
| Layer | Use case | Identity | Cache | Retry | |-------|----------|----------|-------|-------| | KeeperBoardSession | Browser games | Auto-managed | Built-in | Built-in | | KeeperBoardClient | Server-side, advanced | Manual | No | No |
Most browser games should use KeeperBoardSession. Use KeeperBoardClient for server-side code or when you need full control.
KeeperBoardSession API
Constructor
const session = new KeeperBoardSession({
apiKey: 'kb_dev_xxx', // Required
leaderboard: 'main', // Required - session is bound to one board
identity: { keyPrefix: 'app_' }, // Optional localStorage prefix
cache: { ttlMs: 30000 }, // Optional TTL cache for getSnapshot()
retry: { maxAgeMs: 86400000 }, // Optional retry queue for failed submissions
});Identity (auto-managed)
Player names are auto-generated on first access (e.g., BOLDFALCON, SWIFTPANDA). Players can override with setPlayerName().
session.getPlayerGuid(); // Get or create persistent GUID
session.getPlayerName(); // Get stored name (auto-generated if first time)
session.setPlayerName(name); // Store name locally (doesn't update server)
session.hasExplicitPlayerName(); // true if player chose their name
// Validate a name (pure function)
const validated = session.validateName(' Ace Pilot! ');
// Returns 'ACEPILOT' or null if invalidCore Methods
// Submit score (identity auto-injected)
const result = await session.submitScore(1500, { level: 5 });
// Returns: { success: true, rank: 3, isNewHighScore: true }
// or: { success: false, error: 'Network error' }
// Get snapshot (leaderboard + player rank combined)
const snapshot = await session.getSnapshot({ limit: 10 });
// Returns: {
// entries: [{ rank, playerGuid, playerName, score, isCurrentPlayer }],
// totalCount: 150,
// playerRank: { rank: 42, score: 1200, ... } | null // Only if outside top N
// }
// Update player name on server
const success = await session.updatePlayerName('MAVERICK');Retry Queue
// Check for pending scores from previous failed submissions
if (session.hasPendingScore()) {
await session.retryPendingScore();
}Cache
// Pre-fetch in background (e.g., on menu load)
session.prefetch();
// getSnapshot() automatically uses cache when freshEscape Hatch
// Access underlying client for advanced operations
const client = session.getClient();
await client.claimScore({ playerGuid: '...', playerName: '...' });KeeperBoardClient API
Low-level client with options-object methods and camelCase responses.
Constructor
const client = new KeeperBoardClient({
apiKey: 'kb_dev_xxx',
defaultLeaderboard: 'main', // Optional - used when leaderboard not specified
});Methods
// Submit score
const result = await client.submitScore({
playerGuid: 'abc-123',
playerName: 'ACE',
score: 1500,
metadata: { level: 5 }, // Optional
leaderboard: 'weekly', // Optional - overrides defaultLeaderboard
});
// Returns: ScoreResult { id, playerGuid, playerName, score, rank, isNewHighScore }
// Get leaderboard
const lb = await client.getLeaderboard({
leaderboard: 'main', // Optional
limit: 25, // Optional (default 10, max 100)
offset: 0, // Optional pagination
version: 3, // Optional - for time-based boards
});
// Returns: LeaderboardResult { entries, totalCount, resetSchedule, version?, ... }
// Get player rank
const player = await client.getPlayerRank({
playerGuid: 'abc-123',
leaderboard: 'main', // Optional
});
// Returns: PlayerResult | null
// Update player name
const updated = await client.updatePlayerName({
playerGuid: 'abc-123',
newName: 'MAVERICK',
leaderboard: 'main', // Optional
});
// Claim migrated score (for imported data without GUIDs)
const claim = await client.claimScore({
playerGuid: 'abc-123',
playerName: 'OldPlayer',
leaderboard: 'main', // Optional
});
// Health check (no auth required)
const health = await client.healthCheck();Name Validation
Standalone function for validating player names:
import { validateName } from 'keeperboard';
validateName(' Ace Pilot! '); // 'ACEPILOT'
validateName('x'); // null (too short)
validateName('verylongname123456'); // 'VERYLONGNAME' (truncated to 12)
// Custom options
validateName('hello', {
minLength: 3,
maxLength: 8,
uppercase: false,
allowedPattern: /[^a-z]/g,
});Error Handling
import { KeeperBoardError } from 'keeperboard';
try {
await client.submitScore({ ... });
} catch (error) {
if (error instanceof KeeperBoardError) {
switch (error.code) {
case 'INVALID_API_KEY':
console.error('Check your API key');
break;
case 'NOT_FOUND':
console.error('Leaderboard not found');
break;
case 'INVALID_REQUEST':
console.error('Bad request:', error.message);
break;
default:
console.error('API error:', error.message);
}
}
}Anti-Cheat Protection
KeeperBoard provides optional anti-cheat measures to prevent casual leaderboard hacking:
1. HMAC Signing
When enabled, all requests are cryptographically signed to prevent tampering.
const session = new KeeperBoardSession({
apiKey: 'kb_prod_xxx',
leaderboard: 'main',
signingSecret: process.env.KEEPERBOARD_SIGNING_SECRET, // From dashboard
});Setup:
- Enable "HMAC Signing" in KeeperBoard dashboard
- Copy the signing secret
- Add to your game's environment variables
- Pass to SDK constructor
2. Run Tokens
For stronger protection, use run tokens to bind scores to game sessions:
// When game starts
await session.startRun();
// ... player plays the game ...
// When game ends (instead of submitScore)
const result = await session.finishRun(score);
if (result.isNewHighScore) {
console.log('New high score!');
}Server validates:
- Run token exists and hasn't been used
- Minimum elapsed time passed (e.g., 5+ seconds)
- Score is within cap (if configured)
- Signature is valid (if signing enabled)
3. Build Obfuscation
For browser games, obfuscate your production build to make reverse-engineering harder:
// vite.config.js
import obfuscatorPlugin from 'vite-plugin-javascript-obfuscator';
export default {
plugins: [
obfuscatorPlugin({
include: ['src/**/*.ts'],
apply: 'build',
options: {
compact: true,
controlFlowFlattening: true,
stringArray: true,
stringArrayEncoding: ['base64'],
},
}),
],
};Security Model
These measures stop casual cheaters (DevTools interception, simple replay attacks). Determined reverse-engineers with time and skill may still find ways around them. This is an acceptable tradeoff for most indie games.
Phaser.js Integration
import { KeeperBoardSession } from 'keeperboard';
// Initialize once at game start
const leaderboard = new KeeperBoardSession({
apiKey: import.meta.env.VITE_KEEPERBOARD_API_KEY,
leaderboard: 'main',
signingSecret: import.meta.env.VITE_KEEPERBOARD_SIGNING_SECRET, // Optional
cache: { ttlMs: 30000 },
retry: { maxAgeMs: 86400000 },
});
// In BootScene - prefetch and retry
class BootScene extends Phaser.Scene {
async create() {
leaderboard.prefetch();
await leaderboard.retryPendingScore();
this.scene.start('MenuScene');
}
}
// In GameScene - start run for anti-cheat
class GameScene extends Phaser.Scene {
async create() {
await leaderboard.startRun(); // Optional: enables run token validation
// ... game logic ...
}
}
// In GameOverScene - use finishRun if run was started
class GameOverScene extends Phaser.Scene {
async create() {
// finishRun() uses run token if active, falls back to submitScore()
const result = await leaderboard.finishRun(this.score);
if (result.isNewHighScore) {
this.showRank(result.rank, result.isNewHighScore);
}
const snapshot = await leaderboard.getSnapshot({ limit: 10 });
this.displayLeaderboard(snapshot.entries);
}
}Utilities
generatePlayerName
Generate random AdjectiveNoun player names:
import { generatePlayerName } from 'keeperboard';
const name = generatePlayerName(); // 'BOLDFALCON', 'SWIFTPANDA', etc.PlayerIdentity
Standalone helper for localStorage identity management:
import { PlayerIdentity } from 'keeperboard';
const identity = new PlayerIdentity({ keyPrefix: 'myapp_' });
const guid = identity.getOrCreatePlayerGuid();
identity.setPlayerName('ACE');Cache
Generic TTL cache with deduplication:
import { Cache } from 'keeperboard';
const cache = new Cache<Data>(30000); // 30s TTL
const data = await cache.getOrFetch(() => fetchData());RetryQueue
localStorage-based retry for failed operations:
import { RetryQueue } from 'keeperboard';
const queue = new RetryQueue('myapp_retry', 86400000); // 24h max age
queue.save(1500, { level: 5 });
const pending = queue.get(); // { score: 1500, metadata: {...} } or nullDevelopment
# Install dependencies
npm install
# Run tests (requires local KeeperBoard server + Supabase)
npm test
# Type check
npm run typecheck
# Build
npm run buildSee MIGRATION.md for upgrading from v1.x.
License
MIT
