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

scorezilla

v0.5.2

Published

Official Scorezilla SDK — leaderboards for indie games, browser games, and AI-vibe-coded games.

Downloads

740

Readme

scorezilla

npm version bundle size license: MIT CI provenance

Official JavaScript / TypeScript SDK for Scorezilla — focused leaderboard infrastructure for indie games, browser games, and AI-vibe-coded games.

  • Tiny. ~4 KB gzipped. No runtime dependencies.
  • Universal. Browser, Node ≥ 20, Cloudflare Workers, Bun, Deno.
  • Typed. First-class TypeScript with strict types and rich JSDoc.
  • Safe-by-default. Automatic retries on transient failures with idempotency keys; per-request timeouts; cancellation via AbortSignal.
  • Private. No cookies, no localStorage, no fingerprinting beyond runtime detection — see COMPATIBILITY.md.

Status: v0.1.0 ships the public-key client (browser-safe). v0.2.0 ships the HMAC server adapter (scorezilla/server) for game backends that need cheat-resistant submissions. React (scorezilla/react) lands in v0.3.0; Phaser (scorezilla/phaser) in v0.4.0. The first preview is on the next dist-tag — install with npm install scorezilla@next. See CHANGELOG.md and VERSIONING.md.

Commercial context. Scorezilla is a hosted leaderboard service with free and paid tiers — see scorezilla.dev/pricing. This SDK is MIT-licensed and works identically across every plan; get your API key from the operator dashboard.

Install

npm install scorezilla
# or
pnpm add scorezilla
# or
yarn add scorezilla
# or
bun add scorezilla

Get your keys

The Quickstart below needs two values: a publicKey and a boardId. Both come from the dashboard:

  1. Sign in at dashboard.scorezilla.dev (magic-link email — no password).
  2. Open the Tutorial Game that's created for you on first sign-in (or create a new game).
  3. Open Keys → Issue public key. Copy the pk_* string (shown once — also visible in the keys list afterwards).
  4. Open Boards → New board (or use the auto-created "High Scores"). Copy the boardId UUID.

You're now ready for the Quickstart below. Public keys are safe to ship in client code — they only authorize submits, not key rotation or board admin.

Using AI to scaffold? Skip the manual steps: install the Scorezilla MCP server in Claude Code / Cursor and run bootstrap_leaderboard — the AI gets your keys + board id + a ready-to-paste integration snippet in one tool call.

Quickstart

import { Scorezilla, ScorezillaError } from 'scorezilla';

const sz = new Scorezilla({ publicKey: 'pk_mygame_aBcDeF…' });

try {
  const r = await sz.submitScore({
    boardId: 'board-uuid',
    playerId: 'player-uuid',
    score: 9001,
    metadata: { level: 'hard' },
  });
  if (r.isPersonalBest) {
    console.log(`🏆 New personal best! Rank ${r.rank} of ${r.totalEntries}`);
  }
} catch (e) {
  if (e instanceof ScorezillaError && e.isRateLimited()) {
    console.warn(`Rate-limited. Retry after ${e.retryAfter}s.`);
  } else throw e;
}

The four public methods:

await sz.submitScore({ boardId, playerId, score, metadata? });
await sz.getLeaderboard({ boardId, top?, offset? });
await sz.getPlayerRank({ boardId, playerId });
await sz.getWindowAround({ boardId, playerId, before?, after? });

See API.md for the full reference, including every response field, every error code, and advanced patterns.

Headless, never-throws client (scorezilla/headless)

For embedded games that want the same API on every host and never want a failed call to throw, wrap the client in the headless facade. Failures collapse to null (submit) or [] (leaderboard), so a dropped network call can't crash a game loop:

import { createHeadlessClient, isCrossOrigin } from 'scorezilla/headless';

const sz = createHeadlessClient({ publicKey: 'pk_mygame_…' });

// Never throws — `null` means "didn't land", any result means it did.
const result = await sz.submit({ boardId, playerId, score: 4200, name: 'Ada' });
if (result) console.log(`rank ${result.rank} of ${result.totalEntries}`);

// Never throws — `[]` on any failure. Each entry has at least
// { rank, playerId, name?, score }.
const top = await sz.getLeaderboard({ boardId, top: 10 });

When a game is embedded cross-site (itch.io, a portal) and the board has Turnstile gating on, pass a turnstileToken your host obtains (e.g. via a hidden broker iframe on a trusted origin). Use isCrossOrigin(homeOrigin) to decide whether that path is needed; same-origin submits need nothing extra.

Player identity (scorezilla/identity)

Browser-side helpers that produce the playerId you pass to submitScore:

import {
  useAnonymousPlayer, // mints a UUID, persists in localStorage — no prompt
  usePromptedPlayer, // asks once for a nickname, saves it, silent thereafter
  useAuthProvider, // OAuth sign-in (Google + GitHub)
} from 'scorezilla/identity';

const player = await useAuthProvider({
  provider: 'google',
  clientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com',
  storageKey: 'mygame:player',
});
if (player) await sz.submitScore({ boardId, playerId: player.id, score: 9001 });

GitHub needs one extra ingredient: GitHub's token exchange requires your OAuth app's client secret, so you deploy a one-line exchange endpoint (createGitHubOAuthHandler from scorezilla/server) and point the client at it:

// Client — opens a GitHub sign-in popup; resolves github:<id> (or null on decline).
const player = await useAuthProvider({
  provider: 'github',
  clientId: 'YOUR_GITHUB_OAUTH_CLIENT_ID',
  exchangeUrl: '/api/github-oauth',
  storageKey: 'mygame:player',
});

// Server — deploy at /api/github-oauth and register that URL as the OAuth
// app's callback URL on GitHub. The secret + access token never reach the browser.
import { createGitHubOAuthHandler } from 'scorezilla/server';

export const GET = createGitHubOAuthHandler({
  clientId: process.env.GITHUB_CLIENT_ID!,
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
  allowedOrigin: 'https://mygame.example', // your game page's origin
});

Trust boundary — all of these are client-authoritative. useAuthProvider proves the player's identity to the browser, not to the leaderboard: the derived id is computed client-side and submitted with the public key, so it is exactly as forgeable as any other public-key write. OAuth identity is sign-in convenience for casual/vanity boards — not anti-forgery. For competitive or ranking-sensitive boards, submit through the secure path below (createScoreSubmitHandler + a server-verified identity); see RECIPES.md for the combined "OAuth identity + secure submission" recipe.

Server-side HMAC (scorezilla/server)

Public-key submissions are client-authoritative — anyone with your pk_ can submit any score from devtools. For games where ranking matters, sign each submission server-side with a sk_live_* secret:

import { Scorezilla, ScorezillaError } from 'scorezilla/server';

// Single self-contained token from the dashboard. Format:
//   sk_live_<keyId>_<random>
// The SDK parses the keyId out internally; you only manage one value.
const sz = new Scorezilla({
  secretKey: process.env.SCOREZILLA_SECRET_KEY!, // never ship to a browser
});

await sz.submitScore({ boardId, playerId, score, metadata });

Same method shape as the public-key client — submitScore, getLeaderboard, getPlayerRank, getWindowAround. The adapter signs every request with HMAC-SHA256 over a canonical string (method + path + ts + nonce + sha256(body)), and the API verifies before any state change. Replay protection is enforced server-side via a 10-minute nonce window.

The scorezilla/server subpath is server-only — importing it from the browser throws at module evaluation. Use environment variables (or your secret manager) to load the sk_live_* value; never embed it in a build that ships to clients.

Turnkey endpoint: createScoreSubmitHandler

Wiring the secure path by hand means verifying your auth, deriving the player id from it, validating the body, signing, and mapping errors. createScoreSubmitHandler does all of that — you supply only your auth. It returns a standard (Request) => Promise<Response>, so it drops into a Cloudflare Worker, a Next.js route handler, Hono, Deno, or Bun.

import { createScoreSubmitHandler } from 'scorezilla/server';

export const POST = createScoreSubmitHandler({
  secretKey: process.env.SCOREZILLA_SECRET_KEY!,
  boardId: process.env.SCOREZILLA_BOARD_ID!,
  // The one app-specific bit: prove identity, return the TRUSTED playerId.
  // The submitted playerId comes from here — never from the request body.
  verify: async (req) => {
    const user = await myAuth(req);
    return user ? { playerId: user.id } : null;
  },
  cors: { origin: 'https://mygame.example' }, // omit for same-origin
});

Your client just POSTs { score, metadata? } (plus its auth header) to the endpoint; the handler signs and forwards it.

Supabase? One line. Built-in verifiers do the JWKS verification for you. (jose is an optional peer dependency, loaded only when you use a built-in verifier — npm i jose.)

import { createScoreSubmitHandler, verifySupabaseJwt } from 'scorezilla/server';

export const POST = createScoreSubmitHandler({
  secretKey: process.env.SCOREZILLA_SECRET_KEY!,
  boardId: process.env.SCOREZILLA_BOARD_ID!,
  verify: verifySupabaseJwt({ supabaseUrl: process.env.SUPABASE_URL! }),
});

Clerk, Auth0, and Firebase have presets too:

import { verifyClerkJwt, verifyAuth0Jwt, verifyFirebaseIdToken } from 'scorezilla/server';

verify: verifyClerkJwt({ issuer: 'https://clerk.your-app.com' });
verify: verifyAuth0Jwt({ domain: 'you.us.auth0.com', audience: 'your-api' });
verify: verifyFirebaseIdToken({ projectId: 'your-firebase-project' });

Any other JWKS provider uses the generic verifyJwt:

import { verifyJwt } from 'scorezilla/server';

verify: verifyJwt({
  jwksUrl: 'https://your-issuer/.well-known/jwks.json',
  issuer: 'https://your-issuer',
  audience: 'your-api', // claim: 'sub' (default) → playerId
});

Anything else — the verify callback is the universal seam. For Auth.js/NextAuth (encrypted JWE sessions), Better Auth, opaque session cookies, or a provider backend SDK, anything that returns { playerId } works:

verify: async (req) => {
  const session = await validateSession(req); // your DB / SDK
  return session ? { playerId: session.userId } : null;
};

Worked recipes for each of those live in RECIPES.md.

Error handling

Every failure path — HTTP non-2xx, network error, timeout, abort, JSON parse error — throws a single ScorezillaError. Branch on code (machine-stable), never on message (English-only, may change without a major bump):

import { ScorezillaError } from 'scorezilla';

try {
  await sz.submitScore({ boardId, playerId, score });
} catch (e) {
  if (!(e instanceof ScorezillaError)) throw e;

  if (e.isRateLimited()) await sleep((e.retryAfter ?? 30) * 1000);
  else if (e.isAuth()) throw new Error('SDK misconfigured');
  else if (e.code === 'out_of_bounds')
    console.warn(`Score crosses ${e.reason} bound (limit ${e.bound})`);
  else if (e.isTransient()) /* automatic retries already exhausted */ throw e;
  else throw e;
}

The error class carries the request ID, status, and the underlying cause for support tickets:

console.error(`Scorezilla ${e.code} (${e.status}) — req ${e.requestId}`);

Runtime support

| Runtime | Status | Notes | | ---------------------- | ----------------------------- | ------------------------------------------------------- | | Node | ≥ 20 | Hard requirement. Native fetch + crypto.randomUUID. | | Browsers | All evergreen | Chrome 92+, Firefox 95+, Safari 15.4+, Edge 92+. | | Cloudflare Workers | ✅ | Detected via navigator.userAgent. | | Bun | ≥ 1.0 (best-effort in v0.1.0) | Promoted to hard gate in v0.2.0 if stable. | | Deno | ≥ 1.40 | Native fetch + Web Crypto. | | React Native | unverified | Requires react-native-get-random-values polyfill. |

See COMPATIBILITY.md for the detailed matrix, the exactOptionalPropertyTypes workaround, and the privacy invariants.

CDN usage

For zero-build prototyping, import from jsDelivr. Replace <VERSION> with the exact release you want (see the releases page):

<script type="module">
  import { Scorezilla } from 'https://cdn.jsdelivr.net/npm/scorezilla@<VERSION>/dist/index.js';
  const sz = new Scorezilla({ publicKey: 'pk_…' });
  // …
</script>

For production, pair the version pin with Subresource Integrity — the SHA-384 hash for each release ships in the GitHub release notes:

<script
  type="module"
  src="https://cdn.jsdelivr.net/npm/scorezilla@<VERSION>/dist/index.js"
  integrity="sha384-<copy-from-release-notes>"
  crossorigin="anonymous"
></script>

A complete vanilla example lives at examples/vanilla/ and a Node CLI demo at examples/node-cli/. (The examples are in the source repo only — not in the npm tarball.)

Custom fetch / polyfills

Pass your own fetch for environments where the global is missing or you want to mock for tests:

import nodeFetch from 'node-fetch';
const sz = new Scorezilla({ publicKey, fetch: nodeFetch });

The signature (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> is intentionally broader than typeof fetch so node-fetch, undici, vi.fn(), and jest.fn() all typecheck cleanly.

Per-request timeout and retries

const sz = new Scorezilla({
  publicKey,
  timeoutMs: 5_000, // default 30_000
  maxRetries: 1, // default 2 (retries 5xx / 429 / network)
});

Retries automatically reuse the same Idempotency-Key across attempts, so server-side dedup (when added) is safe by default.

Versioning

Strict SemVer from v0.1.0 onward. The machine-stable surface is code strings on ScorezillaError, response field names, method signatures, and config fields. The human-readable message is not part of the contract.

See VERSIONING.md for the full breaking-change rules, deprecation policy, and 0.x → 1.0 exit criteria.

Contributing

Issues and PRs welcome. Local development:

pnpm install
pnpm typecheck
pnpm test                   # all projects
pnpm test:unit              # unit only (fast)
pnpm test:coverage          # unit + coverage gate
pnpm build                  # tsup → dist/
pnpm check:types-resolution # attw — exports map validation
pnpm size                   # size-limit (6 KB gzip ceiling)

Add a release note with pnpm changeset — see .changeset/README.md for the workflow.

License

MIT © isco-tec