OGS for game developers Markdown

profile-kit reference

@open-game-system/profile-kit is how a game learns who is playing and talks to OGS. It has three entry points: the main one (browser, no framework), /react (hooks) and /server (token verification). Every browser call degrades to null or a no-op in a plain browser and on the server, so the same code runs everywhere. Install: Quickstart, step 1.

This page lists every export, and a test fails the docs build when the package gains or loses one. Source: packages/profile-kit/src.

@open-game-system/profile-kit

Use these

getOgsProfileSource

function getOgsProfileSource(): Source<ProfileSnapshot>

The page's OGS profile as an external store (one per page). In the OGS app's WebView it follows the app's profile bridge store, including refreshed tokens. getSnapshot() is undefined while asking the app, null with no profile (plain browser, server), or an OgsProfile. The React hook useOgsProfile() reads it.

getOgsSessionSource

function getOgsSessionSource(): Source<SessionSnapshot>

The couch session from the TV launcher (one per page). Created on first call: it starts listening for ogs:start and posts ogs:ready to the launcher. Call it at module load of the TV entry. getSnapshot() is undefined while waiting, null when the page is not framed by the launcher (after 300 ms, or at once when not framed), or an OgsSession.

onOgsPause

function onOgsPause(onPause: (paused: boolean) => void): () => void

Calls onPause(true) on ogs:suspend (the launcher parked the frame: Home, or another game) and onPause(false) on ogs:resume (Continue). Returns a function that stops listening. Never fires in a plain browser. Only messages from the parent window count.

reportOgsSitting

function reportOgsSitting(report: InstanceReportInput): "bridge" | "launcher" | "none"

Tells OGS about this sitting; its title is the label people see ("Mission 6"). In the OGS app's WebView it dispatches INSTANCE_REPORT to the app's ogs bridge store ("bridge"); framed by the launcher it posts ogs:instance ("launcher"); elsewhere nothing ("none"). Throws if the report fails InstanceReportSchema.

reportOgsRoom

function reportOgsRoom(room: string): "launcher" | "none"

Room-based games: the TV page says which room it shows (ogs:room), so the couch's other phones follow into that room (and, for multiCouch games, other couches can join it). Throws if room is not a valid room id ([A-Za-z0-9_-]{1,64}). "none" when not framed.

ogsRoomFromUrl

function ogsRoomFromUrl(url: string): string | null

Phone page of a room-based game: the room in the ogsRoom query parameter the app adds when this phone joins a room it didn't make (a second phone of the couch following the TV, or a couch joining another couch's room). Join it instead of making one. null when there is none or it is not a valid room id. Use ogsRoomFromUrl(location.href).

readGameToken

function readGameToken(token: string): GameToken | null

Decodes a game token's claims without verifying it, for display only. Anything that matters must use verifyOgsToken on your server.

Types

OgsProfile

type OgsProfile = { id: string; handle: string; name: string; avatar: string; token: string }

The player on this device: profile id, @handle, name, avatar URL and a game token for this game.

ProfileSnapshot

type ProfileSnapshot = OgsProfile | null | undefined

undefined = still asking the app · null = no OGS profile (plain browser) · the profile.

OgsSession

interface OgsSession {
  players: GamePlayer[];
  token: string;
  instanceId: string;
  mode: "continue" | "new";
  room?: string;
}

What the TV page knows from ogs:start: who is on the couch, a game token for the session ("" when OGS could not sign one), the sitting and whether it continues, and the room to join (multiCouch games).

SessionSnapshot

type SessionSnapshot = OgsSession | null | undefined

undefined = waiting for the launcher · null = not on an OGS TV · the session.

GamePlayer

type GamePlayer = { id: string; handle: string; name: string; avatar: string }

Someone on the couch. See Messages and manifest.

InstanceReportInput

type InstanceReportInput = {
  instanceId: string;
  appId: string;
  status: "lobby" | "active" | "suspended" | "waiting" | "completed" | "expired";
  title?: string;
  detail?: string;
  yourTurn?: boolean;
  startsAt?: number;
  resumeUrl?: string;
}

What reportOgsSitting takes: an InstanceReport whose title and detail may be left out (they default to "").

InstanceReport

type InstanceReport = {
  instanceId: string;
  appId: string;
  status: "lobby" | "active" | "suspended" | "waiting" | "completed" | "expired";
  title: string;
  detail: string;
  yourTurn?: boolean;
  startsAt?: number;
  resumeUrl?: string;
}

The parsed report, as sent. Fields: Messages and manifest.

Source

interface Source<T> {
  getSnapshot(): T;
  subscribe(listener: () => void): () => void;
}

An external store, ready for React's useSyncExternalStore (or any framework). getSnapshot is stable while nothing changed.

Lower level (tests and custom wiring)

You rarely need these: the functions above build them for you. They take their dependencies (an app bridge, a window) as arguments, which makes them easy to test with fakes.

createProfileSource

function createProfileSource(opts: ProfileSourceOptions): Source<ProfileSnapshot>

The profile store over any app bridge. Waits timeoutMs (300 ms) for the bridge's profile store before deciding there is none, and up to askingTimeoutMs (5 s) while the app says asking.

ProfileSourceOptions

interface ProfileSourceOptions {
  bridge: ProfileBridge;
  timeoutMs?: number;
  askingTimeoutMs?: number;
}

ProfileBridge

interface ProfileBridge {
  isSupported(): boolean;
  getStore(key: "profile"):
    | { getSnapshot(): unknown; subscribe(listener: (state: unknown) => void): () => void }
    | undefined;
  subscribe(listener: () => void): () => void;
}

What profile-kit needs of an app bridge (app-bridge-web's createWebBridge, or a mock).

ProfileStores

type ProfileStores = { profile: { state: ProfileBridgeState; events: { type: "REFRESH" } } };

The app-bridge store the OGS app gives a game's WebView.

ProfileBridgeState

type ProfileBridgeState =
  | { status: "asking" }
  | { status: "ready"; profile: OgsProfile }
  | { status: "none" };

The profile store's state: asking while the app fetches the game token, ready with it, none when there is no profile or no token for this game.

PROFILE_TIMEOUT_MS

const PROFILE_TIMEOUT_MS = 300

How long to wait for the app's profile store before deciding there is none.

ASKING_TIMEOUT_MS

const ASKING_TIMEOUT_MS = 5000

How long the app may stay asking before the game shows its own form.

createSessionSource

function createSessionSource(opts: { win: FrameWindow; timeoutMs?: number }): Source<SessionSnapshot>

The session store over any window. Listens for ogs:start from win.parent, posts ogs:ready, and settles to null after timeoutMs (300 ms) without a start.

SESSION_TIMEOUT_MS

const SESSION_TIMEOUT_MS = 300

FrameTarget

interface FrameTarget {
  postMessage(message: unknown, targetOrigin: string): void;
}

Something a page can post to (its parent window).

FrameWindow

interface FrameWindow extends FrameTarget {
  parent: FrameTarget | null;
  addEventListener(type: "message", handler: (ev: { data: unknown; source: unknown }) => void): void;
  removeEventListener(type: "message", handler: (ev: { data: unknown; source: unknown }) => void): void;
}

The parts of window the TV side uses (a fake in tests).

listenForPause

function listenForPause(onPause: (paused: boolean) => void, win: FrameWindow): () => void

onOgsPause over any window.

reportOgsInstance

function reportOgsInstance(
  input: InstanceReportInput,
  deps: { bridge: OgsBridge; win: FrameWindow },
): "bridge" | "launcher" | "none"

reportOgsSitting over any bridge and window. If the bridge is supported but its ogs store is not there yet, it dispatches as soon as the store appears.

OgsBridge

interface OgsBridge {
  isSupported(): boolean;
  getStore(key: "ogs"):
    | { dispatch(event: { type: "INSTANCE_REPORT"; report: InstanceReport }): void }
    | undefined;
  subscribe(listener: () => void): () => void;
}

What reporting needs of an app bridge.

OgsStores

type OgsStores = {
  ogs: {
    state: { reported: string[] };
    events: { type: "INSTANCE_REPORT"; report: InstanceReport };
  };
};

The app's ogs bridge store: the page reports its sitting, the app posts it to OGS.

@open-game-system/profile-kit/react

Hooks over the sources above (useSyncExternalStore; during server rendering both are undefined). Needs React 18 or later.

useOgsProfile

function useOgsProfile(source?: Source<ProfileSnapshot>): ProfileSnapshot

Who is playing on this device: undefined while asking the OGS app, null in a plain browser (show your name form), or { id, handle, name, avatar, token }. source defaults to getOgsProfileSource(); pass one in tests.

useOgsSession

function useOgsSession(source?: Source<SessionSnapshot>): SessionSnapshot

On the TV page: undefined while waiting for the launcher, null when not on an OGS TV, or { players, token, instanceId, mode, room? }. source defaults to getOgsSessionSource().

Re-exported types

/react re-exports these types from the main entry, so a React page needs one import:

OgsProfile (react)

Same as OgsProfile.

ProfileSnapshot (react)

Same as ProfileSnapshot.

OgsSession (react)

Same as OgsSession.

SessionSnapshot (react)

Same as SessionSnapshot.

@open-game-system/profile-kit/server

For your game's server (Cloudflare Workers, Node 20+, Deno, Bun: anything with fetch and Web Crypto).

verifyOgsToken

function verifyOgsToken(
  token: string,
  opts: { appId: string } & VerifierOptions,
): Promise<GameToken | null>

Verifies a game token for your game: the ES256 signature against OGS's key set, aud === appId (a token for another game is rejected), and expiry. Returns the claims, or null when any check fails. The key set is cached per jwksUrl; an unknown key id refetches it at most once a minute. Passing fetch or now uses a fresh verifier (tests).

const claims = await verifyOgsToken(token, { appId: "space-bakery", jwksUrl: env.OGS_JWKS_URL });

createOgsVerifier

function createOgsVerifier(opts?: VerifierOptions): (token: string, appId: string) => Promise<GameToken | null>

A verifier with its own cached key set, for when you want to hold one explicitly.

VerifierOptions

interface VerifierOptions {
  jwksUrl?: string;
  fetch?: (url: string) => Promise<Response>;
  now?: () => number;
}

jwksUrl defaults to OGS_JWKS_URL. fetch and now are for tests.

OGS_JWKS_URL

const OGS_JWKS_URL = "https://api.opengame.org/.well-known/jwks.json"

The default key set. Set jwksUrl explicitly from configuration: it lets seam tests serve a local key set, and lets you point at the OGS API you deploy against.

GameToken

type GameToken = {
  iss: string; aud: string; sub: string; handle: string; name: string; avatar: string;
  sid?: string; players?: GamePlayer[]; couch?: CouchClaim; iat: number; exp: number;
}

The claims OGS signs into a game token. sub is the profile id (the host's on a TV token); sid and players are on TV tokens; couch on every token issued for a couch. Never friends, other games, device ids, push tokens or age. Field table: Messages and manifest.

CouchClaim

type CouchClaim = { sid: string; label: string }

The couch a token was issued for: the couch session id and its label (the host's name). Players with the same sid sit on the same couch.