npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

keeperboard

v2.2.2

Published

TypeScript client SDK for KeeperBoard leaderboard-as-a-service

Readme

KeeperBoard SDK

TypeScript client for KeeperBoard leaderboard-as-a-service. Works in browsers and Node.js.

Installation

npm install keeperboard

Quick 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 invalid

Core 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 fresh

Escape 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:

  1. Enable "HMAC Signing" in KeeperBoard dashboard
  2. Copy the signing secret
  3. Add to your game's environment variables
  4. 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 null

Development

# Install dependencies
npm install

# Run tests (requires local KeeperBoard server + Supabase)
npm test

# Type check
npm run typecheck

# Build
npm run build

See MIGRATION.md for upgrading from v1.x.

License

MIT