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

@jahandco/game-sdk

v0.8.1

Published

Jah and Co Game Studio SDK — platform bridge, typed clients, lobby UI, and server runtime for titles

Readme

@jahandco/game-sdk

The title-facing SDK for building games on Jah and Co Game Studio. Provides a typed Bridge protocol for communicating with the platform, domain clients (identity, wallet, lobby, chat, multiplayer, datastore, assets, ai), built-in lobby/roster Web Component UI (<jc-lobby>, <jc-roster-overlay>), and an isolate server runtime for authoritative game logic.

The SDK is framework-agnostic: developers can build titles using any rendering approach or engine they prefer (Canvas 2D, WebGL, Phaser, Three.js, PixiJS, BabylonJS, React, plain DOM, etc.).

npm install @jahandco/game-sdk

Entry points

| Import | Contents | |---|---| | @jahandco/game-sdk | Main entry point — MockBridge, RemoteBridge, connectPlatformBridge, HUD_DEPTH, domain clients (IdentityClient, LobbyClient, ChatClient, MultiplayerClient, WalletClient, DataStoreClient, AssetsClient, AIClient), LOBBY_ELEMENT_TAG/ROSTER_OVERLAY_ELEMENT_TAG/LOBBY_THEME_DEFAULTS, resolveSocialConfig, and everything re-exported from @jahandco/protocol (Bridge, BridgeContracts, BridgeEvents, TICKET_CURRENCY_NAME, TOKEN_CURRENCY_NAME, etc.) | | @jahandco/game-sdk/bridge | connectPlatformBridge, MockBridge, RemoteBridge, and protocol types. | | @jahandco/game-sdk/clients | IdentityClient, LobbyClient, ChatClient, MultiplayerClient, WalletClient, DataStoreClient, AssetsClient, AIClient — import individually if you only want the typed client classes. | | @jahandco/game-sdk/lobby-ui | Registers the <jc-lobby> and <jc-roster-overlay> custom elements (import for side effects, plus exported element classes and theme defaults). | | @jahandco/game-sdk/server | onMessage, publish, publishTo, Secrets — for a title's server-authoritative logic run inside session-isolates sandbox. |

Booting a title

import "@jahandco/game-sdk/lobby-ui";
import type { JcLobbyElement, JcRosterOverlayElement } from "@jahandco/game-sdk/lobby-ui";
import { connectPlatformBridge, MockBridge } from "@jahandco/game-sdk/bridge";
import { IdentityClient, WalletClient } from "@jahandco/game-sdk/clients";

const TITLE_ID = "your-title-slug";

async function boot(): Promise<void> {
  // Connect to the platform cabinet iframe if present, otherwise fall back to MockBridge for local dev
  let bridge;
  try {
    bridge = (await connectPlatformBridge(TITLE_ID)) ?? new MockBridge(TITLE_ID);
  } catch (error) {
    document.body.textContent = error instanceof Error ? error.message : String(error);
    throw error;
  }

  const app = document.getElementById("app")!;

  // For multiplayer titles: mount the built-in lobby UI
  const lobby = document.createElement("jc-lobby") as JcLobbyElement;
  lobby.bridge = bridge;
  lobby.titleId = TITLE_ID;
  app.appendChild(lobby);

  // In-gameplay roster overlay (Esc to open)
  const roster = document.createElement("jc-roster-overlay") as JcRosterOverlayElement;
  roster.bridge = bridge;
  app.appendChild(roster);

  lobby.addEventListener(
    "jc-lobby-ready",
    () => {
      lobby.remove();
      void roster.start();
      void startGame({ mount: app, bridge });
    },
    { once: true },
  );

  await lobby.start();
}

async function startGame({ mount, bridge }: { mount: HTMLElement; bridge: any }) {
  const identity = new IdentityClient(bridge);
  const wallet = new WalletClient(bridge);

  const who = await identity.whoami();
  const balance = await wallet.getWallet();

  console.log(`Ready! Player: ${who.displayName}, Balance: ${balance.tokenBalance} Tokens`);
  // Render your game canvas / initialize your game loop here...
}

void boot();

The Bridge and its domain clients

Every Bridge implements:

bridge.request(domain, action, payload): Promise<response>
bridge.on(event, handler): () => void   // returns an unsubscribe function
bridge.destroy(): void

Typed clients wrap this request/response interface across six domains:

IdentityClient

import { IdentityClient } from "@jahandco/game-sdk/clients";
const identity = new IdentityClient(bridge);

const who = await identity.whoami(); // { userId, displayName }

WalletClient

import { WalletClient } from "@jahandco/game-sdk/clients";
const wallet = new WalletClient(bridge);

await wallet.getWallet();          // { tokenBalance, ticketBalance } — platform-wide
await wallet.getEntryPrice();      // { amount, currency } — flat Tokens cost to play
await wallet.getPerks(titleId?);   // { perks: Perk[] }
await wallet.purchasePerk(perkId); // { success, balance }
await wallet.claimDailyReward();   // { claimed, balance }

Two platform currencies exist:

  • Tokens (TOKEN_CURRENCY_NAME): Spent on entry fees to play titles. The entry fee is charged server-side upon connecting, so title code does not need to handle unpaid participants.
  • Tickets (TICKET_CURRENCY_NAME): Earned from gameplay rewards and spent on in-game perks. Listen for wallet.rewardsGranted events (bridge.on("wallet.rewardsGranted", handler)) for real-time "+N Tickets" feedback.

LobbyClient

import { LobbyClient } from "@jahandco/game-sdk/clients";
const lobby = new LobbyClient(bridge);

await lobby.list(titleId?);          // { lobbies: LobbySummary[] } — open public lobbies
await lobby.create(options?);        // { lobbyId, name, code?, maxPlayers, status, members: LobbyMember[] }
await lobby.join(lobbyIdOrOptions?); // { lobbyId, members: LobbyMember[], ... }
await lobby.leave();
await lobby.setReady(ready);
await lobby.kick(userId);            // host-only
await lobby.start();                 // host-only — "round begins" sync signal

lobby.onMemberJoined(handler);        // (member: LobbyMember) => void
lobby.onMemberLeft(handler);          // ({ userId }) => void
lobby.onMemberReadyChanged(handler);  // ({ userId, ready }) => void
lobby.onHostChanged(handler);         // ({ userId }) => void

ChatClient

import { ChatClient } from "@jahandco/game-sdk/clients";
const chat = new ChatClient(bridge);

await chat.send(text);
chat.onMessage((msg) => console.log(`${msg.displayName}: ${msg.text}`));

MultiplayerClient

import { MultiplayerClient } from "@jahandco/game-sdk/clients";
const multiplayer = new MultiplayerClient(bridge);

await multiplayer.join(sessionId?);
await multiplayer.leave();
await multiplayer.syncState(state);  // state: unknown, your custom shape
multiplayer.onPlayerJoined(handler);
multiplayer.onPlayerLeft(handler);
multiplayer.onStateUpdate(handler);

DataStoreClient

Per-project key/value storage. From a browser client, access is automatically scoped to the calling player's verified userId:

import { DataStoreClient } from "@jahandco/game-sdk/clients";
const datastore = new DataStoreClient(bridge);

await datastore.get(key);                         // { entry: DataStoreEntry | null }
await datastore.set(key, value, expectedVersion); // { version } — optimistic concurrency
await datastore.increment(key, amount);           // { value, version } — atomic increment
await datastore.remove(key, expectedVersion);     // { removed }
await datastore.listKeys({ prefix?, limit? });     // { keys, cursor }

AssetsClient

Reference, load, and render assets uploaded via Developer Studio's Asset Vault (images, audio, video, binary/data files):

import { AssetsClient } from "@jahandco/game-sdk/clients";
const assets = new AssetsClient(bridge);

// 1. Resolve live URL
const url = await assets.getUrl(assetId);

// 2. Load Image (HTMLImageElement) for Canvas 2D or WebGL textures
const image = await assets.loadImage(assetId);
ctx.drawImage(image, 0, 0);

// 3. Load Audio (HTMLAudioElement) for music & sound effects
const sound = await assets.loadAudio(assetId);
sound.play();

// 4. Load Video (HTMLVideoElement) for cutscenes or background video
const video = await assets.loadVideo(assetId, { autoplay: true, loop: true, muted: true });
document.body.appendChild(video);

// 5. Load JSON data or 3D Models / binary buffers
const config = await assets.loadJson(assetId);
const gltfBuffer = await assets.loadArrayBuffer(assetId);

// 6. List all approved assets for this title
const list = await assets.list({ type: "image" });

AIClient

Server-proxied generative AI text for in-game NPC dialogue/decisions. A title's client can never hold a raw model API key (it'd ship in the browser bundle), so this routes through session-service, which holds the one platform-wide Gemini key and returns a structured JSON response:

import { AIClient } from "@jahandco/game-sdk/clients";
const ai = new AIClient(bridge);

const result = await ai.direct({
  intent: "npc_dialogue", // any label your title defines — used server-side to shape the prompt
  speaker: { id: "npc_1", name: "Lord Sinclair", temperament: "diplomatic" },
  roster: [
    { id: "npc_1", name: "Lord Sinclair" },
    { id: "npc_2", name: "Lady Vance", suspicionScore: 40 },
  ],
  history: ["Lady Vance: I don't trust the quiet ones.", "..."],
  context: { day: 3, missionSabotaged: true },
});

console.log(result.content);        // in-character line
console.log(result.targetId);       // present when you asked the model to pick one — validate it's in your own roster before acting on it
console.log(result.suspicionDelta); // advisory only, ~-15..15

Rate-limited per connection server-side. Always have a local fallback for when the call fails or times out — treat it like any other network request, not a guaranteed response. MockBridge returns a canned placeholder line for local dev, since it can't hold the real key either.

Built-in Lobby UI & Review Gating

The platform provides <jc-lobby> and <jc-roster-overlay> Web Components via @jahandco/game-sdk/lobby-ui.

import "@jahandco/game-sdk/lobby-ui";

const lobby = document.createElement("jc-lobby");
lobby.bridge = bridge;
lobby.titleId = TITLE_ID;
lobby.chatEnabled = true;
document.getElementById("app")?.appendChild(lobby);

lobby.addEventListener("jc-lobby-ready", () => {
  lobby.remove();
  // start game logic
});

await lobby.start();

Theming

Restyle the lobby UI via CSS custom properties on the <jc-lobby> host:

jc-lobby {
  --jc-lobby-bg: #111318;
  --jc-lobby-surface: #1a1d24;
  --jc-lobby-surface-border: #2c313d;
  --jc-lobby-text: #f0f2f5;
  --jc-lobby-text-muted: #8b949e;
  --jc-lobby-accent: #3b82f6;
  --jc-lobby-radius-panel: 12px;
}

Play Mode Review Gating

When publishing a build, automated checks verify that multiplayer titles (realtime_multiplayer, turn_based_multiplayer) include lobby code, while single-player titles do not.

Server-authoritative logic (@jahandco/game-sdk/server)

Authoritative game logic runs server-side inside session-isolates' sandbox (isolated-vm, 128MB limit). It interacts via onMessage and publish:

import { onMessage, publish } from "@jahandco/game-sdk/server";

onMessage((event, payload) => {
  if (event === "playerAction") {
    // Process authoritative simulation...
    publish("stateSync", { /* ... */ });
  }
});

publish("ready", {});

Reporting Match Outcome

To report the final outcome of a session, publish a payload matching GameCompletedPayloadSchema (re-exported from @jahandco/protocol):

publish("multiplayer.gameCompleted", {
  winnerUserId: "user_123",
  placements: [{ userId: "user_123", place: 1 }],
  participantUserIds: ["user_123", "user_456"],
});

Secrets

Per-project, per-environment values configured in Developer Studio (Project → Secrets in the sidebar, or jc secrets) — an API key, a signing secret, a third-party account id, anything your server logic needs but shouldn't have hardcoded in dist/server/server.js. Resolved fresh and injected into the sandbox at session start; never baked into the bundle on disk and never reachable from a title's browser client (there's no Secrets on MockBridge/RemoteBridge — this only exists in @jahandco/game-sdk/server):

import { Secrets } from "@jahandco/game-sdk/server";

const apiKey = Secrets.get("STRIPE_SECRET_KEY"); // string | undefined
if (!Secrets.has("STRIPE_SECRET_KEY")) {
  publish("error", { message: "STRIPE_SECRET_KEY not configured for this environment" });
}

session-isolates runs with no outbound network access at all (the same reason AIClient above is server-proxied instead of calling Gemini directly), so a secret read this way can't be used to make an HTTP call from inside your game logic — it's for in-sandbox use: HMAC/webhook signature checks, feature-gating, anything that doesn't require reaching another host. Secrets.get returns undefined for a key that isn't set for the session's current environment (development vs. production) — there's no throwing variant, so always check before assuming a value is present.

Manage secrets with the CLI:

jc secrets set STRIPE_SECRET_KEY sk_live_...      # development by default
jc secrets set STRIPE_SECRET_KEY sk_live_... --production
jc secrets list [--production]
jc secrets remove <secretId>

Values are write-only once saved — Developer Studio and jc secrets list only ever show key names, never values back out.

Local development & MockBridge

When developing locally without the Game Studio platform:

  • connectPlatformBridge resolves undefined in standalone dev servers.
  • Fall back to new MockBridge(titleId) for fully offline, in-memory local testing with simulated identity, wallet, and echo chat.

Title Architecture & Manual Scaffolding Guide

While jc game init or npx @jahandco/create-game scaffolds titles automatically, developers can manually structure or customize their titles.

Ideal Project Structure (React + SSR)

my-title/
├── index.html               # Client HTML template with <!--ssr-outlet-->
├── server.js                # Main application & SSR server (Vite middleware in dev, SSR in prod)
├── public/
│   ├── style.css
│   └── assets/              # Static game assets (sprites, audio, models)
├── src/
│   ├── App.tsx              # Universal React game component
│   ├── main.tsx             # Universal app factory
│   ├── entry-client.tsx     # Client hydration, Bridge & <jc-lobby> bootstrap
│   ├── entry-server.tsx     # Server-side HTML render (renderToString)
│   └── game/
│       └── server.ts        # Authoritative multiplayer game logic (anti-cheat, math)
├── vite.config.ts           # Vite config with @vitejs/plugin-react
├── package.json             # Build and dev scripts
├── tsconfig.json
└── jc.config.json           # Developer Studio project link (schemaVersion 1)

Unified Build Output (dist/)

Titles compile all outputs into a single parent dist/ directory with clean client and server separation:

dist/
├── client/                     # Public client build (Vite)
│   ├── index.html
│   ├── assets/
│   │   ├── entry-client-*.js
│   │   └── ...
│   └── style.css
└── server/                     # Server logic (private)
    ├── entry-server.js         # Universal React SSR render bundle (Vite SSR)
    └── server.js               # Authoritative game logic bundle (bundled automatically by CLI)
  1. dist/client/ (Public): Produced by vite build --outDir dist/client. Contains the browser bundle, styles, and assets. When uploaded, these files are served to players' web browsers in Game Studio.
  2. dist/server/server.js (Private): Produced by this title's own build (scripts/build-server.mjs, esbuild) from src/game/server.ts — not by the jc CLI, which only reads it (jc upload) or watches it (jc dev). A single self-contained IIFE bundle for session-isolates (raw V8 sandbox). Kept private from browser downloads.
  3. dist/server/entry-server.js (Private): Produced by vite build --outDir dist/server --ssr src/entry-server.tsx. Used by server.js for SSR rendering.

Step-by-Step Manual Scaffolding

1. Configure package.json

{
  "name": "my-title",
  "private": true,
  "version": "0.0.1",
  "type": "module",
  "scripts": {
    "dev": "node server.js",
    "build:client": "vite build --outDir dist/client",
    "build:server": "vite build --outDir dist/server --ssr src/entry-server.tsx",
    "build": "vite build --outDir dist/client && vite build --outDir dist/server --ssr src/entry-server.tsx",
    "preview": "NODE_ENV=production node server.js",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@jahandco/game-sdk": "^0.6.0",
    "@jahandco/protocol": "^0.3.0",
    "express": "^4.21.2",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/node": "^22.7.5",
    "@types/react": "^18.3.11",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.2",
    "typescript": "^5.6.3",
    "vite": "^5.4.8"
  }
}

2. Create src/game/server.ts (Authoritative Game Logic)

import { onMessage, publish } from "@jahandco/game-sdk/server";

let currentScore = 0;

onMessage((event, payload: any) => {
  if (event === "player_action") {
    currentScore += 1;
    publish("state_sync", { score: currentScore, message: "Action accepted by server" });
  } else if (event === "ping") {
    publish("pong", { echo: payload, at: Date.now() });
  }
});

publish("ready", { score: currentScore });

3. Create server.js (SSR Application Server)

import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import express from "express";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const isProduction = process.env.NODE_ENV === "production";
const port = process.env.PORT || 5173;
const base = process.env.BASE || "/";

async function createServer() {
  const app = express();

  let vite;
  if (!isProduction) {
    const { createServer: createViteServer } = await import("vite");
    vite = await createViteServer({
      server: { middlewareMode: true },
      appType: "custom",
      base,
    });
    app.use(vite.middlewares);
  } else {
    app.use(base, express.static(path.resolve(__dirname, "dist/client"), { index: false }));
  }

  app.use("*", async (req, res, next) => {
    const url = req.originalUrl.replace(base, "");
    try {
      let template;
      let render;

      if (!isProduction) {
        template = await fs.readFile(path.resolve(__dirname, "index.html"), "utf-8");
        template = await vite.transformIndexHtml(url, template);
        render = (await vite.ssrLoadModule("/src/entry-server.tsx")).render;
      } else {
        template = await fs.readFile(path.resolve(__dirname, "dist/client/index.html"), "utf-8");
        render = (await import("./dist/server/entry-server.js")).render;
      }

      const rendered = await render(url);
      const html = template.replace("<!--ssr-outlet-->", rendered.html ?? "");

      res.status(200).set({ "Content-Type": "text/html" }).send(html);
    } catch (e) {
      if (vite) vite.ssrFixStacktrace(e);
      next(e);
    }
  });

  return { app };
}

createServer().then(({ app }) =>
  app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}`);
  }),
);

4. Create index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Title</title>
    <link rel="stylesheet" href="/style.css" />
  </head>
  <body>
    <div id="app"><!--ssr-outlet--></div>
    <script type="module" src="/src/entry-client.tsx"></script>
  </body>
</html>

5. Link with Developer Studio (jc.config.json)

{
  "schemaVersion": 1,
  "projectId": "proj_123",
  "projectName": "My Title",
  "projectSlug": "my-title",
  "template": "default",
  "serverEntry": "src/game/server.ts"
}

Run jc upload to build and upload your title to Developer Studio.