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

@4players/odin-cortex

v1.13.0

Published

Official TypeScript SDK for the ODIN Cortex API

Readme

@4players/odin-cortex

Official TypeScript SDK for the ODIN Cortex API — real-time voice transcription, AI-powered analysis, content moderation, gatherings, and serverless functions for ODIN Voice applications.

Installation

npm install @4players/[email protected]

Quick Start

import { CortexClient } from "@4players/odin-cortex";

const client = new CortexClient({
  baseUrl: "https://cortex.odin.4players.io",
  apiKey: "ots_live_...",
});

const project = client.project("your-project-id");

// Create and start a session
const session = await project.sessions.create({
  title: "My Session",
  idleTimeout: 60,
  externalRoomId: "room-1",
});
await session.start();

// Later: stop and retrieve messages
await session.stop();
const messages = await session.getMessages();

Authentication

The SDK supports four authentication methods. The first three are backend credentials — never ship them in a game client. The fourth, the participant token, is the one that is meant for a client. You can manage your credentials in the ODIN Cortex Dashboard.

API Key

Best for server-side integrations, CI/CD pipelines, and third-party applications. Create and manage API keys in the dashboard under your account settings.

const client = new CortexClient({
  baseUrl: "https://cortex.odin.4players.io",
  apiKey: "ots_live_...",
});

Project Secret (HMAC-SHA256)

Best for ODIN platform services and server-to-server communication. You can find your project secret in the dashboard under your project settings.

Every request is signed with HMAC-SHA256 automatically. The SDK sends X-Project-Id, X-Signature, and X-Timestamp headers on each request.

const client = new CortexClient({
  baseUrl: "https://cortex.odin.4players.io",
  projectId: "your-project-id",
  projectSecret: "your-project-secret",
});

JWT Bearer Token

Best for dashboard users and browser-based sessions. Obtain a token via the login endpoints.

const client = new CortexClient({
  baseUrl: "https://cortex.odin.4players.io",
  accessToken: "eyJ...",
});

Participant Token (game clients)

For end-user devices. Your backend mints a short-lived, Ed25519-signed token for one player; the device carries it and can reach only that player's own data, through client.me.

// --- On your backend (API key or project secret) ---
const { token, expiresAt, odinToken } = await backend
  .project("your-project-id")
  .participants.issueToken({
    externalUserId: "steam:76561198000000000",
    displayName: "Alice",
    roomId: "boss-room-17",     // optional: also returns the ODIN voice token
    gatheringId: lobby.id,      // optional: narrows the token to one lobby
  });

// --- In the game client ---
const cortex = new CortexClient({
  baseUrl: "https://cortex.odin.4players.io",
  participantToken: token,
});

const me = await cortex.me.identity();
const { muted, banned } = await cortex.me.sanctions();

let after = 0;
const page = await cortex.me.listMessages(sessionId, { after }, { ifNoneMatch: `"${after}"` });
if (page) after = page.latestSeq;   // `null` means 304 Not Modified — nothing new

Tokens live at most 15 minutes, so fetch a new one from your backend before expiresAt. Issuing requires the participants.token scope. To invalidate every token of a project at once (a leaked backend credential, not a single ban), call project.participants.revokeAllTokens().

Other services can verify a participant token themselves against the public keys at GET /api/.well-known/jwks.json — no call to Cortex needed.

Usage

The SDK is organized around resource managers accessed through a project scope. Methods like create() and get() return live objects: every field from the API response is available directly on the object (e.g. session.title, session.status), and the object also carries action methods (start(), stop(), …) and knows its own identity.

const session = await project.sessions.get(sessionId);

session.title;   // ✅ fields are on the object directly
session.status;
await session.start(); // ✅ ...and so are the action methods

// session.data.title still works but is @deprecated — prefer session.title.

Live objects are also plain data: JSON.stringify(session) returns exactly the API payload (no client internals), and a Session is assignable to SessionResponse.

Project Scope

All project-scoped resources are accessed through client.project(projectId):

const project = client.project("your-project-id");

Sessions

// List all sessions
const sessions = await project.sessions.list();

// Create a session (returns a live Session object)
const session = await project.sessions.create({
  title: "Team Meeting",
  idleTimeout: 120,
  externalRoomId: "room-42",
});

// Live object methods
await session.start();
const messages = await session.getMessages();

// Polling with a cursor + ETag — the cheap way to follow a live transcript
// from a backend or a serverless function (a 304 costs Cortex one Redis read)
let after = 0;
const page = await session.listMessages(
  { after, limit: 200, include: ["annotations"] },
  { ifNoneMatch: `"${after}"` }
);
if (page) {
  console.log(page.messages, page.latestSeq); // store latestSeq as the next `after`
}

// Sessions one page at a time (newest first), optionally by status
const active = await project.sessions.listPaged({ status: "active", limit: 20 });
await session.regenerateSummary();
await session.stop();

Gatherings

const gathering = await project.gatherings.create({
  name: "Daily Standup",
});

// Members (sub-resource on the live object)
await gathering.addMember({ participantId: "p1" });
await gathering.sendInvitations({ participantIds: ["p2", "p3"] });
const members = await gathering.listMembers();

// Lifecycle
await gathering.start({ roomId: "room-1" });
await gathering.end();

// Join by code
const result = await project.gatherings.joinByCode({
  joinCode: "ABCD1234",
  participantId: "p1",
});

Serverless Functions

// Create and deploy a function
const fn = await project.functions.create({
  name: "on-message",
  code: "export default async (ctx) => { ... }",
});
await fn.publish({ changelog: "Initial version" });
await fn.deploy();

// Runtime management
await project.functions.startRuntime();
const status = await project.functions.getRuntimeStatus();
await project.functions.stopRuntime();

// Settings and dependencies
await project.functions.updateDependencies({
  dependencies: [{ name: "lodash", version: "^4.17" }],
});
await project.functions.updateEnvVars({
  envVars: [{ key: "API_URL", value: "https://..." }],
});

Participants and Sanctions

// Participants
const participant = await project.participants.create({
  externalUserId: "user-123",
  displayName: "Alice",
});
await participant.update({ displayName: "Alice Smith" });

// Sanctions
const sanction = await project.sanctions.create({
  participantId: participant.id,
  type: "mute",
  reason: "Disruptive behavior",
});
await sanction.revoke({ reason: "Warning acknowledged" });

// Lookup
const active = await project.sanctions.getActive("user-123");

// Join gate: issuing a participant token refuses banned players
import { CortexJoinRefusedError } from "@4players/odin-cortex";
try {
  const { token, odinToken, odinTokenRestrictions } = await project.participants.issueToken({
    externalUserId: "user-123",
    roomId: "room-1",
  });
  // odinTokenRestrictions.tags contains "cortex:muted" for a muted player
} catch (e) {
  if (e instanceof CortexJoinRefusedError) console.log("banned until", e.sanction.endAt ?? "forever");
  else throw e;
}

Webhooks

Cortex delivers domain events to a URL you host. Every outbound request is signed with HMAC-SHA256 using a secret you provide here — your receiver verifies the X-Odin-Signature header using the same value. This webhook secret is separate from the 4Players project secret used to authenticate the SDK client itself; it only protects traffic that flows from Cortex to your URL.

import { randomBytes } from "node:crypto";

// Pick any random string; store it alongside your webhook receiver so it can
// verify incoming signatures. This is NOT the 4Players project secret.
const webhookSecret = randomBytes(32).toString("hex");

const sub = await project.webhooks.createSubscription({
  url: "https://example.com/webhook",
  events: ["session.created", "session.updated", "message.created"],
  secret: webhookSecret,
});

// listEvents() returns the delivery queue (pending / delivered / failed
// attempts) — not a catalog of subscribable event names.
const events = await project.webhooks.listEvents();

Optional fields on createSubscription — authType, apiKey, apiKeyHeader, basicAuthUsername, basicAuthPassword, customHeaders — configure how Cortex authenticates to your webhook URL when it posts events. Leave authType unset (defaults to "none") if your receiver validates only via X-Odin-Signature.

See Domain Events for the full event catalog — common names include session.created, session.updated, message.created, participant.joined, gathering.created.

ODIN Voice Tokens

// Generate a token for client-side ODIN Voice
const token = await project.generateToken({
  roomId: "room-1",
  userId: "user-1",
});

Room Push

The Cortex bot sits in the ODIN room of every live session, so a backend can push messages to the players over the voice connection they already have — no extra token, endpoint or connection on the client. Requires the rooms.push scope; limits are 8 KB per frame and 20 messages per second per room.

// From a backend, or from a serverless function as ctx.cortex.rooms.send(...)
await project.rooms.send(sessionId, {
  type: "match.countdown",            // anything except the reserved "cortex." prefix
  data: { secondsLeft: 10 },
  targetExternalUserIds: ["user-1"],  // optional; omit to reach everyone in the room
});

Clients receive a Cortex Room Protocol frame through ODIN's MessageReceived, sent from the bot peer: the bytes 0x43 0x58 0x01 ("CX", version 1) followed by JSON { type, id, seq, data }. Besides your own messages, the bot sends cortex.transcript (live transcript, when the project's transcriptPush setting is on) and cortex.sanction / cortex.mutes (when sanctionPush is on). A web or Node client can decode them with the bundled reference decoder:

import { decodeCortexRoomFrame, CortexRoomFrameType } from "@4players/odin-cortex";

room.onMessageReceived(({ senderPeerId, message }) => {
  if (senderPeerId !== botPeerId) return;            // everything else is your app's own traffic
  const frame = decodeCortexRoomFrame(message);
  if (!frame) return;                                // not a Cortex frame
  if (frame.type === CortexRoomFrameType.TRANSCRIPT) showCaption(frame.data);
});

Push is the fast path, not the source of truth: after a reconnect, catch up with session.listMessages({ after: lastSeq }).

Live Updates (watch)

Every list() / get() returns a one-shot snapshot. To receive live updates instead, call watch() — a Firestore-style subscription that loads the initial state via REST and then keeps it in sync over Server-Sent Events (SSE). No boilerplate: no manual EventSource, no event filtering, no merge logic.

watch() returns a subscription handle:

const project = client.project("my-project-id");

// Collection: watch all sessions
const sub = project.sessions.watch();

sub.onSnapshot((sessions, changes) => {
  // `sessions` is the full current array (kept in sync)
  // `changes` is the diff that triggered this snapshot
  console.log(`${sessions.length} sessions`);
  for (const c of changes) console.log(c.type, c.id); // "added" | "modified" | "removed"
});

sub.onError((err) => console.error(err));

console.log(sub.current); // latest array, read synchronously at any time

sub.unsubscribe(); // stop listening (and release the shared connection)

Watch a single object by passing an id, or call .watch() on a live object. The value is undefined until the initial fetch resolves, and again if the object is deleted:

const sub = project.sessions.watch(sessionId);
sub.onSnapshot((session) => console.log(session?.status));

// equivalently, from a live object:
const session = await project.sessions.get(sessionId);
const sub2 = session.watch();

Supported resources. Collections and single objects are watched from the manager (project.<resource>); nested collections are watched from a live object you fetch first (just like listMembers() / getMessages()):

// Collection, or a single object by id — from the manager
project.sessions.watch(); //      project.sessions.watch(id)
project.participants.watch(); //  project.participants.watch(id)
project.sanctions.watch(); //     project.sanctions.watch(id)   (revoked stays, status updated)
project.gatherings.watch(); //    project.gatherings.watch(id)

// A single object can also be watched from its own live object
const session = await project.sessions.get(sessionId);
session.watch();

// Nested collections live on a live object (not on project.gatherings directly)
session.watchMessages(); // session transcript — new messages stream in

const gathering = await project.gatherings.get(gatheringId);
gathering.watchMembers(); //     members, keyed by participantId
gathering.watchInvitations(); // invitation status updates live

Resilience. All watchers for a project share a single SSE connection (ref-counted; closed when the last watcher unsubscribes). The connection auto-reconnects with exponential backoff, and on every (re)connect each subscription re-fetches its snapshot to resync — so a dropped connection never leaves you with stale data. A client-side idle timeout (default 45s, configurable via realtime.idleTimeoutMs) guards against dead sockets:

const client = new CortexClient({
  baseUrl,
  apiKey: "ots_live_...",
  realtime: { idleTimeoutMs: 45_000 }, // 0 to disable
});

Live updates work with all three auth mechanisms (API key, JWT, project secret). Streaming requires a WHATWG fetch — available as the global fetch on Node ≥ 18 and in all modern browsers.

Non-Project-Scoped Resources

Some resources are accessed directly on the client:

// Authentication
const { accessToken } = await client.auth.loginWithCredentials({
  email: "[email protected]",
  password: "...",
});
const profile = await client.auth.getProfile();

// API keys
const keys = await client.apiKeys.list();
// The scope catalog: every grantable scope (family, label, sensitivity) plus presets
const { scopes, presets } = await client.apiKeys.getScopes();
const newKey = await client.apiKeys.create({
  name: "Game backend",
  // e.g. the "Game backend" preset: mint player tokens and check sanctions
  scopes: ["participants.token", "sanctions.read"],
});
await client.apiKeys.revoke(newKey.id);

// Plugin catalog
const plugins = await client.pluginCatalog.list();
const plugin = await client.pluginCatalog.get("profanity-filter");

// Health check
await client.healthCheck();

Debug Mode

Enable request/response logging for troubleshooting:

const client = new CortexClient({
  baseUrl: "https://cortex.odin.4players.io",
  apiKey: "ots_live_...",
  debug: true,
});

This logs every request and response (method, URL, status, body) to console.debug.

Error Handling

The SDK throws typed errors based on HTTP status codes:

import {
  CortexAuthError, // 401, 403
  CortexNotFoundError, // 404
  CortexValidationError, // 422
  CortexRateLimitError, // 429
  CortexApiError, // Other HTTP errors
  CortexError, // Base error class
} from "@4players/odin-cortex";

try {
  await project.sessions.get("nonexistent-id");
} catch (err) {
  if (err instanceof CortexNotFoundError) {
    console.log("Session not found");
  }
}

Requirements

  • Node.js 18+
  • TypeScript 5.4+ (for type-checking)

License

MIT