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

castle-web-sdk

v0.4.6

Published

`castle-web-sdk` lets a deck use services provided by the Castle platform — for example, a deck can save per-player data, post and read scores on a leaderboard, or get a server-synced time for daily content.

Readme

Castle Web SDK Reference

castle-web-sdk lets a deck use services provided by the Castle platform — for example, a deck can save per-player data, post and read scores on a leaderboard, or get a server-synced time for daily content.

Comes with castle-web init. Import what you need from castle-web-sdk:

import { setup, initCard, Storage, Leaderboard } from "castle-web-sdk";

Contents

Storage

Storage saves data for the current player. Nobody else can read it. Use it for save files, settings, progress.

Storage.get<T>(key): Promise<T | null>

Returns the value at key, or null if not set.

const level = (await Storage.get("level")) ?? 1;

Storage.set(key, value)

Sets key to value. value must be something that can convert to JSON (null, booleans, finite numbers, strings, arrays, plain objects). The next get(key) returns the new value immediately. Writes save in the background.

Storage.set("level", 7);
Storage.set("settings", { sound: true, music: false });

Storage.remove(key)

Removes key.

SharedStorage

SharedStorage saves data that other players can read. Values must be something that can convert to JSON, same as Storage.

Scopes:

  • 'deck' — one shared bucket for the whole deck. Any player can read or write.
  • 'user' — a per-player public bucket. Any player can read; only the owning player can write.

SharedStorage.get(scope, key): Promise<T | null>

Reads a shared value. For 'user', omit the user id to read the current player's bucket, or pass one to read someone else's:

const worldHighScore = await SharedStorage.get("deck", "highScore");
const myColor = await SharedStorage.get("user", "color");
const theirColor = await SharedStorage.get("user", otherUserId, "color");

SharedStorage.set(scope, key, value)

Writes a shared value. 'user' writes always go to the current player's bucket. Writes save in the background.

SharedStorage.set("deck", "highScore", 9001);
SharedStorage.set("user", "color", "red");

SharedStorage.remove(scope, key)

Removes a shared value.

Leaderboard

Leaderboard ranks players by a numeric score, per deck and per variable name. Pick a variable name for each leaderboard the deck has (e.g. 'score', 'time').

Leaderboard.write(variable, score, options?)

Submits a score. Only the player's best score for that variable (and scope) is kept. In the editor it does nothing — safe to call from gameplay code unconditionally.

options is { scope?: string }. By default the score goes to the deck's global leaderboard for variable; pass scope to write to a separate leaderboard (for example a daily one, or a custom id):

Leaderboard.write("score", 1200);

// A daily leaderboard: scope by the current Castle day so each day gets
// its own board (see Time.getServerDate).
const { daysSinceCastleEpoch } = await Time.getServerDate();
Leaderboard.write("score", 1200, { scope: `daily-${daysSinceCastleEpoch}` });

Leaderboard.fetch(variable, type, options?): Promise<LeaderboardData>

Fetches the leaderboard for variable. type is 'high' (highest first) or 'low' (lowest first). options.scope works the same as in write.

If the player has written a score this session, a fetch reflects their own new score right away — you can write then fetch and show the result without waiting. Other players' recent scores still appear on their own normal timing.

The returned LeaderboardData has:

  • list — array of entries, each { place, value, username, userId? }.
  • playerRank — the current player's place on the board (if they have a score).
  • playerValue — the current player's score (if they have one).
const data = await Leaderboard.fetch("score", "high");
for (const entry of data.list) {
  console.log(`${entry.place}. ${entry.username} — ${entry.value}`);
}
if (data.playerRank) {
  console.log(`you are #${data.playerRank} with ${data.playerValue}`);
}

Time

Time.getServerTime(): Promise<number>

Returns the current server time as a Unix timestamp in seconds.

const now = await Time.getServerTime();

Time.getServerDate(timezone?): Promise<CastleDateParts>

Returns the current server time broken into date parts. timezone is 'Castle' (default; Castle's server timezone, same for every player) or 'player' (the player's local timezone).

The returned CastleDateParts has:

  • sec, min, hour — time of day.
  • day (1-31), month (1-12), year — date.
  • wday — day of the week (1-7, Sunday = 1).
  • yday — day of the year (1-366).
  • daysSinceCastleEpoch — a day number that increments every day. Use it for daily content.
const date = await Time.getServerDate("player");
const dailyPuzzle = (date.daysSinceCastleEpoch % 30) + 1;

User

User.getCurrent(): Promise<CastleUser>

Returns the signed-in player. Throws CastleError (LOGIN_REQUIRED) when nobody is signed in.

The returned CastleUser has userId, username, and isActive.

const me = await User.getCurrent();
greet(me.username);

Pass

A pass is something a creator sells to players for Castle bricks (the in-app currency): buy it once, own it for good. Use one to gate part of a deck behind a purchase — bonus levels, a cosmetic, supporting the creator. Set up the pass (name, art, price) on Castle; a deck refers to it by id.

Pass.has(passId): Promise<boolean>

Returns true if the current player owns the pass. No UI, nothing charged — use it to gate content.

if (await Pass.has(bonusLevelsPassId)) {
  showBonusLevels();
}

Pass.offer(passId): Promise<PassOfferResult>

Presents the pass for the player to buy; resolves when they're done. Bricks cost real money, so this only works in the Castle mobile app — elsewhere (the website, the dev server) it resolves unavailable. PassOfferResult has a status:

  • 'purchased' — just bought it; grant access.
  • 'alreadyOwned' — already had it (not charged); grant access.
  • 'cancelled' — dismissed without buying.
  • 'unavailable' — can't buy here (e.g. the website).
const { status } = await Pass.offer(bonusLevelsPassId);
if (status === "purchased" || status === "alreadyOwned") {
  showBonusLevels();
}

Setup

Startup, editor-mode check, and a file-write call for editor UI.

setup()

Call this once at the start of the deck, before any other SDK call. setup() initializes the SDK so the rest of the API is usable and mounts the centered 5:7 card shell around whatever the deck renders into #root (when the deck is being played standalone in a browser). While running locally with castle-web serve, it also forwards console output to the CLI and reloads the page when castle-web restart runs.

import { setup } from "castle-web-sdk";

setup();

initCard(): HTMLDivElement

Use this when the deck draws into a <canvas> (or anything else) rather than into the React tree at #root. Returns a centered, viewport-sized <div> with the standard Castle 5:7 card aspect ratio. The div resizes itself when the window resizes.

import { setup, initCard } from "castle-web-sdk";

setup();
const card = initCard();

const canvas = document.createElement("canvas");
canvas.style.cssText = "width: 100%; height: 100%; display: block;";
card.appendChild(canvas);

If the deck mounts a React tree into #root instead, you don't need initCard()setup() already wraps #root's children in a card.

CARD_RATIO

The card aspect ratio (5 / 7). Use this if you need to size something to match the card.

isEdit(): boolean

true when the deck is being edited, false when it's being played. Use this to show editor UI only in edit mode.

import { isEdit, setup } from "castle-web-sdk";

setup();
if (isEdit()) {
  mountEditor();
} else {
  startGame();
}

writeFile(path, contents): Promise<void>

Writes a file in the deck directory. path is relative to the deck root, contents is a string. Use this from editor UI to save scenes, drawings, or generated source.

import { writeFile } from "castle-web-sdk";

await writeFile("scenes/main.scene", JSON.stringify(scene, null, 2));

Only works while editing locally with castle-web serve. Calls from a published deck fail.

CastleError

Every error the SDK throws is a CastleError. Check code to tell the kinds apart.

Common codes:

  • LOGIN_REQUIRED — the player needs to be signed in.
  • MISSING_DECK_ID — the deck hasn't been saved to Castle yet, so it has no id.
  • CASTLE_STORAGE_SERIALIZE_FAILED — value isn't plain JSON (e.g. a class instance, a function, a non-finite number, or a cycle).
  • INVALID_LEADERBOARD_VARIABLE, INVALID_LEADERBOARD_SCORE, INVALID_LEADERBOARD_TYPE — bad argument to a Leaderboard call.
  • UNSUPPORTED_TIMEZONETime.getServerDate got a zone other than 'Castle' or 'player'.
  • CASTLE_HOST_UNAVAILABLE — the Castle host (the app or website running the deck) didn't handle the request — e.g. it timed out or wasn't reachable. Usually transient; retry or surface a gentle error.