@lastshotlabs/slingshot-game-engine
npm install @lastshotlabs/slingshot-game-engine
Functions
Section titled “Functions”authorizeDisplayToken
Section titled “authorizeDisplayToken”Display tokens — casting a game to a real screen.
The problem
Section titled “The problem”Every game has a TV view: the big screen the room looks at. Until now none of them could actually be cast. Open the TV route on a Chromecast, a smart-TV browser, or any device that has never logged in, and every request 401s. It only appeared to work because the host opened the TV as a tab in their own already-authenticated browser, silently borrowing their session. In a music game where the TV is the speaker, that meant no TV and therefore no sound.
The threat model — say it out loud
Section titled “The threat model — say it out loud”A display token lives in a URL, on a screen, in somebody’s living room. Guests can read it off the TV. It will be photographed. Assume it leaks.
So a leaked display token must be a NON-EVENT. It grants exactly one thing: read-only visibility of ONE game session, exactly as a spectator would see it. It is not a login. It cannot be widened into one:
- The actor it produces has
kind: 'display'andid: null. Slingshot’suserAuthrequireskind === 'user'AND a non-nullid, so a display token can never satisfyuserAuth— on ANY route, in ANY package, present or future. Read-only is therefore structural, not a check somebody has to remember to write. Every app route that guards ongetActorId(c)already rejects it today, before those apps know display tokens exist. - It is bound to one
sessionId. It is useless against any other session. - It carries no user identity, no roles, and no claims beyond its own session.
- It expires, it dies when the session ends, and the host can revoke it.
A display token is a key to a window, not a key to the house.
Format
Section titled “Format”d1.<base64url(payload)>.<hmac-sha256-hex>
payload = { sid, exp, ep, jti } — session id, expiry (epoch ms), the
session’s display epoch (see revocation), and a random id for logging.
The payload is signed, not encrypted: none of it is secret. Signing is what
stops a TV from editing sid and watching someone else’s party.
Revocation, without a new table
Section titled “Revocation, without a new table”The session record carries a displayEpoch counter. A token embeds the epoch
it was minted under; verification requires an exact match. Revoking every
outstanding token for a session is therefore a single increment — no
revocation list, no storage that can drift out of sync with reality, and no
way for a stale token to survive a bump.
function authorizeDisplayToken(claims: DisplayTokenClaims, session: DisplaySessionFacts | null,): DisplayTokenVerificationSource: packages/slingshot-game-engine/src/lib/displayToken.ts
buildContentValidationGuard
Section titled “buildContentValidationGuard”Content validation guard middleware.
Validates that the content provider exists in the game definition
and that the content input passes the provider’s schema.
Rejects with CONTENT_PROVIDER_NOT_FOUND or CONTENT_VALIDATION_FAILED.
function buildContentValidationGuard({ getSessionAdapter, getRegistry, }: ContentValidationGuardDeps): voidSource: packages/slingshot-game-engine/src/middleware/contentValidationGuard.ts
buildLobbyOnlyGuard
Section titled “buildLobbyOnlyGuard”Lobby-only guard middleware.
Rejects requests if the session status is not 'lobby'.
Used on operations that are only valid before the game starts
(e.g., updateRules, updateContent, assignTeam, assignRole).
function buildLobbyOnlyGuard({ getSessionAdapter }: LobbyOnlyGuardDeps): voidSource: packages/slingshot-game-engine/src/middleware/lobbyOnlyGuard.ts
buildRulesValidationGuard
Section titled “buildRulesValidationGuard”Rules validation guard middleware.
Resolves the game definition from the registry, applies any preset,
and validates the rules update against the game’s Zod schema.
Rejects with RULES_VALIDATION_FAILED or PRESET_NOT_FOUND on failure.
function buildRulesValidationGuard({ getSessionAdapter, getRegistry, }: RulesValidationGuardDeps): voidSource: packages/slingshot-game-engine/src/middleware/rulesValidationGuard.ts
createGameEnginePackage
Section titled “createGameEnginePackage”Game engine package factory.
Creates a SlingshotPackageDefinition that registers the GameSession and
GamePlayer entities, wires game-specific guard middleware, mounts the
game-registry and session routes, registers the WS endpoint, and manages
the closure-owned game registry, active session runtimes, replay store,
and cleanup sweep.
Every adapter ref, middleware closure, registry, and timer is owned by the factory’s closure (Rule 3) — multiple package instances in the same process do not share state.
function createGameEnginePackage(rawConfig: Partial<GameEnginePluginConfig> & { games?: GameDefinition[] } = {},): SlingshotPackageDefinitionSource: packages/slingshot-game-engine/src/plugin.ts
createGameSessionPolicy
Section titled “createGameSessionPolicy”Game session access policy.
Uses definePolicyDispatch() from slingshot-entity for extensible
session access control dispatched by gameType.
Other plugins can register game-type-specific access handlers via
registerGameSessionHandler().
function createGameSessionPolicy(): voidSource: packages/slingshot-game-engine/src/policy/index.ts
DEFAULT_DISPLAY_TOKEN_TTL_MS
Section titled “DEFAULT_DISPLAY_TOKEN_TTL_MS”Display tokens — casting a game to a real screen.
The problem
Section titled “The problem”Every game has a TV view: the big screen the room looks at. Until now none of them could actually be cast. Open the TV route on a Chromecast, a smart-TV browser, or any device that has never logged in, and every request 401s. It only appeared to work because the host opened the TV as a tab in their own already-authenticated browser, silently borrowing their session. In a music game where the TV is the speaker, that meant no TV and therefore no sound.
The threat model — say it out loud
Section titled “The threat model — say it out loud”A display token lives in a URL, on a screen, in somebody’s living room. Guests can read it off the TV. It will be photographed. Assume it leaks.
So a leaked display token must be a NON-EVENT. It grants exactly one thing: read-only visibility of ONE game session, exactly as a spectator would see it. It is not a login. It cannot be widened into one:
- The actor it produces has
kind: 'display'andid: null. Slingshot’suserAuthrequireskind === 'user'AND a non-nullid, so a display token can never satisfyuserAuth— on ANY route, in ANY package, present or future. Read-only is therefore structural, not a check somebody has to remember to write. Every app route that guards ongetActorId(c)already rejects it today, before those apps know display tokens exist. - It is bound to one
sessionId. It is useless against any other session. - It carries no user identity, no roles, and no claims beyond its own session.
- It expires, it dies when the session ends, and the host can revoke it.
A display token is a key to a window, not a key to the house.
Format
Section titled “Format”d1.<base64url(payload)>.<hmac-sha256-hex>
payload = { sid, exp, ep, jti } — session id, expiry (epoch ms), the
session’s display epoch (see revocation), and a random id for logging.
The payload is signed, not encrypted: none of it is secret. Signing is what
stops a TV from editing sid and watching someone else’s party.
Revocation, without a new table
Section titled “Revocation, without a new table”The session record carries a displayEpoch counter. A token embeds the epoch
it was minted under; verification requires an exact match. Revoking every
outstanding token for a session is therefore a single increment — no
revocation list, no storage that can drift out of sync with reality, and no
way for a stale token to survive a bump.
Source: packages/slingshot-game-engine/src/lib/displayToken.ts
GAME_ENGINE_PLUGIN_STATE_KEY
Section titled “GAME_ENGINE_PLUGIN_STATE_KEY”Plugin state key and runtime state contract for slingshot-game-engine.
Plugin state is stored in getContext(app).pluginState keyed by
GAME_ENGINE_PLUGIN_STATE_KEY (Rule 15, Rule 16).
Source: packages/slingshot-game-engine/src/types/state.ts
GAME_SESSION_POLICY_KEY
Section titled “GAME_SESSION_POLICY_KEY”Game session access policy.
Uses definePolicyDispatch() from slingshot-entity for extensible
session access control dispatched by gameType.
Other plugins can register game-type-specific access handlers via
registerGameSessionHandler().
Source: packages/slingshot-game-engine/src/policy/index.ts
gameSessionOperations
Section titled “gameSessionOperations”GameSession operations.
Declarative database operations using defineOperations() + op.*().
Each operation produces a typed adapter method AND an auto-generated
HTTP route. Operation names match the keys in GameSession.routes.operations.
See spec §2.4.2 for the full contract.
Source: packages/slingshot-game-engine/src/operations/session.ts
getDisplaySessionId
Section titled “getDisplaySessionId”The HTTP half of display tokens: minting, revoking, and turning a token on a
request into a kind: 'display' actor.
See displayToken.ts for the threat model. The single most important property
is repeated here because everything else depends on it:
The display actor has id: null. Slingshot’s userAuth requires
kind === 'user' AND a non-null id, so a display token cannot satisfy
userAuth anywhere, in any package, now or later. Every existing app route
that guards on getActorId(c) already rejects it — including routes written
before display tokens existed. Apps opt IN explicitly; they never opt out.
function getDisplaySessionId(c: Context<AppEnv>): string | nullSource: packages/slingshot-game-engine/src/lib/displayRuntime.ts
hostRoom
Section titled “hostRoom”Display router.
Decides which WS rooms to publish to and what data to include.
The actual transport is slingshot’s ctx.wsPublish responsibility.
Supports static relay configs, custom relay filters, and dynamic
relay re-evaluation per message for stream channels.
See spec §13 for the full contract.
function hostRoom(sessionId: string): stringSource: packages/slingshot-game-engine/src/lib/display.ts
isDisplayFor
Section titled “isDisplayFor”The HTTP half of display tokens: minting, revoking, and turning a token on a
request into a kind: 'display' actor.
See displayToken.ts for the threat model. The single most important property
is repeated here because everything else depends on it:
The display actor has id: null. Slingshot’s userAuth requires
kind === 'user' AND a non-null id, so a display token cannot satisfy
userAuth anywhere, in any package, now or later. Every existing app route
that guards on getActorId(c) already rejects it — including routes written
before display tokens existed. Apps opt IN explicitly; they never opt out.
function isDisplayFor(c: Context<AppEnv>, sessionId: string): booleanSource: packages/slingshot-game-engine/src/lib/displayRuntime.ts
mintDisplayToken
Section titled “mintDisplayToken”Display tokens — casting a game to a real screen.
The problem
Section titled “The problem”Every game has a TV view: the big screen the room looks at. Until now none of them could actually be cast. Open the TV route on a Chromecast, a smart-TV browser, or any device that has never logged in, and every request 401s. It only appeared to work because the host opened the TV as a tab in their own already-authenticated browser, silently borrowing their session. In a music game where the TV is the speaker, that meant no TV and therefore no sound.
The threat model — say it out loud
Section titled “The threat model — say it out loud”A display token lives in a URL, on a screen, in somebody’s living room. Guests can read it off the TV. It will be photographed. Assume it leaks.
So a leaked display token must be a NON-EVENT. It grants exactly one thing: read-only visibility of ONE game session, exactly as a spectator would see it. It is not a login. It cannot be widened into one:
- The actor it produces has
kind: 'display'andid: null. Slingshot’suserAuthrequireskind === 'user'AND a non-nullid, so a display token can never satisfyuserAuth— on ANY route, in ANY package, present or future. Read-only is therefore structural, not a check somebody has to remember to write. Every app route that guards ongetActorId(c)already rejects it today, before those apps know display tokens exist. - It is bound to one
sessionId. It is useless against any other session. - It carries no user identity, no roles, and no claims beyond its own session.
- It expires, it dies when the session ends, and the host can revoke it.
A display token is a key to a window, not a key to the house.
Format
Section titled “Format”d1.<base64url(payload)>.<hmac-sha256-hex>
payload = { sid, exp, ep, jti } — session id, expiry (epoch ms), the
session’s display epoch (see revocation), and a random id for logging.
The payload is signed, not encrypted: none of it is secret. Signing is what
stops a TV from editing sid and watching someone else’s party.
Revocation, without a new table
Section titled “Revocation, without a new table”The session record carries a displayEpoch counter. A token embeds the epoch
it was minted under; verification requires an exact match. Revoking every
outstanding token for a session is therefore a single increment — no
revocation list, no storage that can drift out of sync with reality, and no
way for a stale token to survive a bump.
function mintDisplayToken(input: { readonly sessionId: string; readonly epoch: number; readonly secret: string | readonly string[]; readonly ttlMs?: number; readonly now?: number; readonly tokenId?: string; }): voidSource: packages/slingshot-game-engine/src/lib/displayToken.ts
playerRoom
Section titled “playerRoom”Display router.
Decides which WS rooms to publish to and what data to include.
The actual transport is slingshot’s ctx.wsPublish responsibility.
Supports static relay configs, custom relay filters, and dynamic
relay re-evaluation per message for stream channels.
See spec §13 for the full contract.
function playerRoom(sessionId: string, userId: string): stringSource: packages/slingshot-game-engine/src/lib/display.ts
registerGameSessionPolicies
Section titled “registerGameSessionPolicies”Game session access policy.
Uses definePolicyDispatch() from slingshot-entity for extensible
session access control dispatched by gameType.
Other plugins can register game-type-specific access handlers via
registerGameSessionHandler().
function registerGameSessionPolicies(app: Hono<AppEnv>): voidSource: packages/slingshot-game-engine/src/policy/index.ts
roleRoom
Section titled “roleRoom”Display router.
Decides which WS rooms to publish to and what data to include.
The actual transport is slingshot’s ctx.wsPublish responsibility.
Supports static relay configs, custom relay filters, and dynamic
relay re-evaluation per message for stream channels.
See spec §13 for the full contract.
function roleRoom(sessionId: string, roleName: string): stringSource: packages/slingshot-game-engine/src/lib/display.ts
sessionRoom
Section titled “sessionRoom”Display router.
Decides which WS rooms to publish to and what data to include.
The actual transport is slingshot’s ctx.wsPublish responsibility.
Supports static relay configs, custom relay filters, and dynamic
relay re-evaluation per message for stream channels.
See spec §13 for the full contract.
function sessionRoom(sessionId: string): stringSource: packages/slingshot-game-engine/src/lib/display.ts
spectatorRoom
Section titled “spectatorRoom”Display router.
Decides which WS rooms to publish to and what data to include.
The actual transport is slingshot’s ctx.wsPublish responsibility.
Supports static relay configs, custom relay filters, and dynamic
relay re-evaluation per message for stream channels.
See spec §13 for the full contract.
function spectatorRoom(sessionId: string): stringSource: packages/slingshot-game-engine/src/lib/display.ts
streamRoom
Section titled “streamRoom”Display router.
Decides which WS rooms to publish to and what data to include.
The actual transport is slingshot’s ctx.wsPublish responsibility.
Supports static relay configs, custom relay filters, and dynamic
relay re-evaluation per message for stream channels.
See spec §13 for the full contract.
function streamRoom(sessionId: string, channelName: string): stringSource: packages/slingshot-game-engine/src/lib/display.ts
teamRoom
Section titled “teamRoom”Display router.
Decides which WS rooms to publish to and what data to include.
The actual transport is slingshot’s ctx.wsPublish responsibility.
Supports static relay configs, custom relay filters, and dynamic
relay re-evaluation per message for stream channels.
See spec §13 for the full contract.
function teamRoom(sessionId: string, teamName: string): stringSource: packages/slingshot-game-engine/src/lib/display.ts
verifyDisplayToken
Section titled “verifyDisplayToken”Display tokens — casting a game to a real screen.
The problem
Section titled “The problem”Every game has a TV view: the big screen the room looks at. Until now none of them could actually be cast. Open the TV route on a Chromecast, a smart-TV browser, or any device that has never logged in, and every request 401s. It only appeared to work because the host opened the TV as a tab in their own already-authenticated browser, silently borrowing their session. In a music game where the TV is the speaker, that meant no TV and therefore no sound.
The threat model — say it out loud
Section titled “The threat model — say it out loud”A display token lives in a URL, on a screen, in somebody’s living room. Guests can read it off the TV. It will be photographed. Assume it leaks.
So a leaked display token must be a NON-EVENT. It grants exactly one thing: read-only visibility of ONE game session, exactly as a spectator would see it. It is not a login. It cannot be widened into one:
- The actor it produces has
kind: 'display'andid: null. Slingshot’suserAuthrequireskind === 'user'AND a non-nullid, so a display token can never satisfyuserAuth— on ANY route, in ANY package, present or future. Read-only is therefore structural, not a check somebody has to remember to write. Every app route that guards ongetActorId(c)already rejects it today, before those apps know display tokens exist. - It is bound to one
sessionId. It is useless against any other session. - It carries no user identity, no roles, and no claims beyond its own session.
- It expires, it dies when the session ends, and the host can revoke it.
A display token is a key to a window, not a key to the house.
Format
Section titled “Format”d1.<base64url(payload)>.<hmac-sha256-hex>
payload = { sid, exp, ep, jti } — session id, expiry (epoch ms), the
session’s display epoch (see revocation), and a random id for logging.
The payload is signed, not encrypted: none of it is secret. Signing is what
stops a TV from editing sid and watching someone else’s party.
Revocation, without a new table
Section titled “Revocation, without a new table”The session record carries a displayEpoch counter. A token embeds the epoch
it was minted under; verification requires an exact match. Revoking every
outstanding token for a session is therefore a single increment — no
revocation list, no storage that can drift out of sync with reality, and no
way for a stale token to survive a bump.
function verifyDisplayToken(token: string, opts: { readonly secret: string | readonly string[]; readonly now?: number },): DisplayTokenVerificationSource: packages/slingshot-game-engine/src/lib/displayToken.ts
Constants
Section titled “Constants”GameEngine
Section titled “GameEngine”Public contract for slingshot-game-engine.
Cross-package consumers resolve GameEngineRuntimeCap through
ctx.capabilities.require(...) to read the active game-engine state
(session controls, registry, adapters). For backward compatibility the
runtime is also published to pluginState under GAME_ENGINE_PLUGIN_STATE_KEY
— that path is preserved during the bridge period.
Source: packages/slingshot-game-engine/src/public.ts
gameEnginePluginConfigSchema
Section titled “gameEnginePluginConfigSchema”Zod validation schema for GameEnginePluginConfig.
Validated at plugin construction time via validatePluginConfig()
and frozen via deepFreeze() (Rule 10).
Source: packages/slingshot-game-engine/src/validation/config.ts
GameEnginePluginConfigSchema
Section titled “GameEnginePluginConfigSchema”Zod validation schema for GameEnginePluginConfig.
Validated at plugin construction time via validatePluginConfig()
and frozen via deepFreeze() (Rule 10).
Config Fields
Section titled “Config Fields”| Field | Description |
|---|---|
cleanup | Cleanup configuration for completed/abandoned sessions. |
disableRoutes | Routes to disable. Keys are entityName.operationOrAction strings. |
disconnect | Default disconnect configuration. |
heartbeat | WS heartbeat configuration. |
mountPath | Mount path for game engine REST routes. Default: /game. |
recovery | WS message persistence and recovery configuration. |
wsEndpoint | WS endpoint name. Default: ‘game’. |
wsRateLimit | WS rate-limiting configuration (per-socket, rolling window). |
Source: packages/slingshot-game-engine/src/validation/config.ts
GameEngineRuntimeCap
Section titled “GameEngineRuntimeCap”Public contract for slingshot-game-engine.
Cross-package consumers resolve GameEngineRuntimeCap through
ctx.capabilities.require(...) to read the active game-engine state
(session controls, registry, adapters). For backward compatibility the
runtime is also published to pluginState under GAME_ENGINE_PLUGIN_STATE_KEY
— that path is preserved during the bridge period.
Source: packages/slingshot-game-engine/src/public.ts
GameErrorCode
Section titled “GameErrorCode”Game engine error codes and error class.
Every error the engine produces — REST and WS — uses a code from this registry. Codes are string constants that clients can switch on for i18n or UI logic. See spec §28.4 for the full registry.
Source: packages/slingshot-game-engine/src/errors.ts
GamePlayer
Section titled “GamePlayer”GamePlayer entity definition.
Defines the persisted player entity with all fields, indexes, route configuration, permissions, and event declarations.
See spec §2.4.1, §6.1, and §26.3 for the full contract.
Source: packages/slingshot-game-engine/src/entities/gamePlayer.ts
gamePlayerFactories
Section titled “gamePlayerFactories”Entity factories for GameSession and GamePlayer.
Uses createEntityFactories() from slingshot-entity to produce
RepoFactories<T> dispatched by StoreType (Rule 17).
At startup the plugin’s buildAdapter callbacks call resolveRepo()
with these factories to get the concrete adapter for the configured
store type.
See spec §2.4.3 for the full contract.
Source: packages/slingshot-game-engine/src/entities/factories.ts
gamePlayerOperations
Section titled “gamePlayerOperations”GamePlayer operations.
Declarative database operations using defineOperations() + op.*().
Each operation produces a typed adapter method AND an auto-generated
HTTP route. Operation names match the keys in GamePlayer.routes.operations.
See spec §2.4.2 for the full contract.
Source: packages/slingshot-game-engine/src/operations/player.ts
GameSession
Section titled “GameSession”GameSession entity definition.
Defines the persisted session entity with all fields, indexes, route configuration, permissions, and event declarations.
See spec §2.4.1, §5.1, and §26.2 for the full contract.
Source: packages/slingshot-game-engine/src/entities/gameSession.ts
gameSessionFactories
Section titled “gameSessionFactories”Entity factories for GameSession and GamePlayer.
Uses createEntityFactories() from slingshot-entity to produce
RepoFactories<T> dispatched by StoreType (Rule 17).
At startup the plugin’s buildAdapter callbacks call resolveRepo()
with these factories to get the concrete adapter for the configured
store type.
See spec §2.4.3 for the full contract.
Source: packages/slingshot-game-engine/src/entities/factories.ts
Classes
Section titled “Classes”GameError
Section titled “GameError”Game engine error codes and error class.
Every error the engine produces — REST and WS — uses a code from this registry. Codes are string constants that clients can switch on for i18n or UI logic. See spec §28.4 for the full registry.
Source: packages/slingshot-game-engine/src/errors.ts
Interfaces
Section titled “Interfaces”BufferedInput
Section titled “BufferedInput”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ChannelDefinition
Section titled “ChannelDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ChannelRuntimeState
Section titled “ChannelRuntimeState”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ContentDefinition
Section titled “ContentDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ContentProvider
Section titled “ContentProvider”Provider interfaces for the game engine.
These are swappable contracts (Rule 8) for replay storage, content provision, and per-player rate limiting.
Source: packages/slingshot-game-engine/src/types/adapters.ts
ContentProviderDefinition
Section titled “ContentProviderDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
DisplaySessionFacts
Section titled “DisplaySessionFacts”Display tokens — casting a game to a real screen.
The problem
Section titled “The problem”Every game has a TV view: the big screen the room looks at. Until now none of them could actually be cast. Open the TV route on a Chromecast, a smart-TV browser, or any device that has never logged in, and every request 401s. It only appeared to work because the host opened the TV as a tab in their own already-authenticated browser, silently borrowing their session. In a music game where the TV is the speaker, that meant no TV and therefore no sound.
The threat model — say it out loud
Section titled “The threat model — say it out loud”A display token lives in a URL, on a screen, in somebody’s living room. Guests can read it off the TV. It will be photographed. Assume it leaks.
So a leaked display token must be a NON-EVENT. It grants exactly one thing: read-only visibility of ONE game session, exactly as a spectator would see it. It is not a login. It cannot be widened into one:
- The actor it produces has
kind: 'display'andid: null. Slingshot’suserAuthrequireskind === 'user'AND a non-nullid, so a display token can never satisfyuserAuth— on ANY route, in ANY package, present or future. Read-only is therefore structural, not a check somebody has to remember to write. Every app route that guards ongetActorId(c)already rejects it today, before those apps know display tokens exist. - It is bound to one
sessionId. It is useless against any other session. - It carries no user identity, no roles, and no claims beyond its own session.
- It expires, it dies when the session ends, and the host can revoke it.
A display token is a key to a window, not a key to the house.
Format
Section titled “Format”d1.<base64url(payload)>.<hmac-sha256-hex>
payload = { sid, exp, ep, jti } — session id, expiry (epoch ms), the
session’s display epoch (see revocation), and a random id for logging.
The payload is signed, not encrypted: none of it is secret. Signing is what
stops a TV from editing sid and watching someone else’s party.
Revocation, without a new table
Section titled “Revocation, without a new table”The session record carries a displayEpoch counter. A token embeds the epoch
it was minted under; verification requires an exact match. Revoking every
outstanding token for a session is therefore a single increment — no
revocation list, no storage that can drift out of sync with reality, and no
way for a stale token to survive a bump.
Source: packages/slingshot-game-engine/src/lib/displayToken.ts
DisplayTokenClaims
Section titled “DisplayTokenClaims”Display tokens — casting a game to a real screen.
The problem
Section titled “The problem”Every game has a TV view: the big screen the room looks at. Until now none of them could actually be cast. Open the TV route on a Chromecast, a smart-TV browser, or any device that has never logged in, and every request 401s. It only appeared to work because the host opened the TV as a tab in their own already-authenticated browser, silently borrowing their session. In a music game where the TV is the speaker, that meant no TV and therefore no sound.
The threat model — say it out loud
Section titled “The threat model — say it out loud”A display token lives in a URL, on a screen, in somebody’s living room. Guests can read it off the TV. It will be photographed. Assume it leaks.
So a leaked display token must be a NON-EVENT. It grants exactly one thing: read-only visibility of ONE game session, exactly as a spectator would see it. It is not a login. It cannot be widened into one:
- The actor it produces has
kind: 'display'andid: null. Slingshot’suserAuthrequireskind === 'user'AND a non-nullid, so a display token can never satisfyuserAuth— on ANY route, in ANY package, present or future. Read-only is therefore structural, not a check somebody has to remember to write. Every app route that guards ongetActorId(c)already rejects it today, before those apps know display tokens exist. - It is bound to one
sessionId. It is useless against any other session. - It carries no user identity, no roles, and no claims beyond its own session.
- It expires, it dies when the session ends, and the host can revoke it.
A display token is a key to a window, not a key to the house.
Format
Section titled “Format”d1.<base64url(payload)>.<hmac-sha256-hex>
payload = { sid, exp, ep, jti } — session id, expiry (epoch ms), the
session’s display epoch (see revocation), and a random id for logging.
The payload is signed, not encrypted: none of it is secret. Signing is what
stops a TV from editing sid and watching someone else’s party.
Revocation, without a new table
Section titled “Revocation, without a new table”The session record carries a displayEpoch counter. A token embeds the epoch
it was minted under; verification requires an exact match. Revoking every
outstanding token for a session is therefore a single increment — no
revocation list, no storage that can drift out of sync with reality, and no
way for a stale token to survive a bump.
Source: packages/slingshot-game-engine/src/lib/displayToken.ts
GameDefinition
Section titled “GameDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
GameDefinitionInput
Section titled “GameDefinitionInput”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
GameDisconnectConfig
Section titled “GameDisconnectConfig”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
GameEngineActiveSessionSnapshot
Section titled “GameEngineActiveSessionSnapshot”Plugin state key and runtime state contract for slingshot-game-engine.
Plugin state is stored in getContext(app).pluginState keyed by
GAME_ENGINE_PLUGIN_STATE_KEY (Rule 15, Rule 16).
Source: packages/slingshot-game-engine/src/types/state.ts
GameEngineAdvancePhaseInput
Section titled “GameEngineAdvancePhaseInput”Plugin state key and runtime state contract for slingshot-game-engine.
Plugin state is stored in getContext(app).pluginState keyed by
GAME_ENGINE_PLUGIN_STATE_KEY (Rule 15, Rule 16).
Source: packages/slingshot-game-engine/src/types/state.ts
GameEnginePluginConfig
Section titled “GameEnginePluginConfig”Plugin configuration for slingshot-game-engine.
Validated via GameEnginePluginConfigSchema at plugin construction time
and frozen via deepFreeze() (Rule 10).
Source: packages/slingshot-game-engine/src/types/config.ts
GameEnginePluginState
Section titled “GameEnginePluginState”Plugin state key and runtime state contract for slingshot-game-engine.
Plugin state is stored in getContext(app).pluginState keyed by
GAME_ENGINE_PLUGIN_STATE_KEY (Rule 15, Rule 16).
Source: packages/slingshot-game-engine/src/types/state.ts
GameEngineSessionControls
Section titled “GameEngineSessionControls”Plugin state key and runtime state contract for slingshot-game-engine.
Plugin state is stored in getContext(app).pluginState keyed by
GAME_ENGINE_PLUGIN_STATE_KEY (Rule 15, Rule 16).
Source: packages/slingshot-game-engine/src/types/state.ts
GameEngineSessionMutationContext
Section titled “GameEngineSessionMutationContext”Plugin state key and runtime state contract for slingshot-game-engine.
Plugin state is stored in getContext(app).pluginState keyed by
GAME_ENGINE_PLUGIN_STATE_KEY (Rule 15, Rule 16).
Source: packages/slingshot-game-engine/src/types/state.ts
GameEngineSessionMutationResult
Section titled “GameEngineSessionMutationResult”Plugin state key and runtime state contract for slingshot-game-engine.
Plugin state is stored in getContext(app).pluginState keyed by
GAME_ENGINE_PLUGIN_STATE_KEY (Rule 15, Rule 16).
Source: packages/slingshot-game-engine/src/types/state.ts
GameEngineSubmitInput
Section titled “GameEngineSubmitInput”Plugin state key and runtime state contract for slingshot-game-engine.
Plugin state is stored in getContext(app).pluginState keyed by
GAME_ENGINE_PLUGIN_STATE_KEY (Rule 15, Rule 16).
Source: packages/slingshot-game-engine/src/types/state.ts
GameLifecycleHooks
Section titled “GameLifecycleHooks”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
GameLoopDefinition
Section titled “GameLoopDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
GamePlayerState
Section titled “GamePlayerState”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
GameSessionState
Section titled “GameSessionState”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
GameTimer
Section titled “GameTimer”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
HandlerResult
Section titled “HandlerResult”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
InputAck
Section titled “InputAck”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
Leaderboard
Section titled “Leaderboard”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
LifecycleHooks
Section titled “LifecycleHooks”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
PhaseDefinition
Section titled “PhaseDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
PlayerInfo
Section titled “PlayerInfo”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ProcessHandlerContext
Section titled “ProcessHandlerContext”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
RateLimitBackend
Section titled “RateLimitBackend”Provider interfaces for the game engine.
These are swappable contracts (Rule 8) for replay storage, content provision, and per-player rate limiting.
Source: packages/slingshot-game-engine/src/types/adapters.ts
ReadonlyHandlerContext
Section titled “ReadonlyHandlerContext”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ReplayEntry
Section titled “ReplayEntry”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ReplayStore
Section titled “ReplayStore”Provider interfaces for the game engine.
These are swappable contracts (Rule 8) for replay storage, content provision, and per-player rate limiting.
Source: packages/slingshot-game-engine/src/types/adapters.ts
RoleAssignmentContext
Section titled “RoleAssignmentContext”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
RoleDefinition
Section titled “RoleDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ScheduledEvent
Section titled “ScheduledEvent”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ScoreEntry
Section titled “ScoreEntry”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ScoringDefinition
Section titled “ScoringDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
SeededRng
Section titled “SeededRng”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
SessionLeaseAdapter
Section titled “SessionLeaseAdapter”Provider interfaces for the game engine.
These are swappable contracts (Rule 8) for replay storage, content provision, and per-player rate limiting.
Source: packages/slingshot-game-engine/src/types/adapters.ts
SessionMutex
Section titled “SessionMutex”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
SubPhaseDefinition
Section titled “SubPhaseDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
SyncDefinition
Section titled “SyncDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
TeamDefinition
Section titled “TeamDefinition”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
TeamScoreEntry
Section titled “TeamScoreEntry”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
TurnState
Section titled “TurnState”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
VoteTally
Section titled “VoteTally”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
WinResult
Section titled “WinResult”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ChannelFromConfig
Section titled “ChannelFromConfig”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ChannelMode
Section titled “ChannelMode”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ChannelRelayConfig
Section titled “ChannelRelayConfig”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ClientToServerMessage
Section titled “ClientToServerMessage”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
DisplayTokenFailure
Section titled “DisplayTokenFailure”Display tokens — casting a game to a real screen.
The problem
Section titled “The problem”Every game has a TV view: the big screen the room looks at. Until now none of them could actually be cast. Open the TV route on a Chromecast, a smart-TV browser, or any device that has never logged in, and every request 401s. It only appeared to work because the host opened the TV as a tab in their own already-authenticated browser, silently borrowing their session. In a music game where the TV is the speaker, that meant no TV and therefore no sound.
The threat model — say it out loud
Section titled “The threat model — say it out loud”A display token lives in a URL, on a screen, in somebody’s living room. Guests can read it off the TV. It will be photographed. Assume it leaks.
So a leaked display token must be a NON-EVENT. It grants exactly one thing: read-only visibility of ONE game session, exactly as a spectator would see it. It is not a login. It cannot be widened into one:
- The actor it produces has
kind: 'display'andid: null. Slingshot’suserAuthrequireskind === 'user'AND a non-nullid, so a display token can never satisfyuserAuth— on ANY route, in ANY package, present or future. Read-only is therefore structural, not a check somebody has to remember to write. Every app route that guards ongetActorId(c)already rejects it today, before those apps know display tokens exist. - It is bound to one
sessionId. It is useless against any other session. - It carries no user identity, no roles, and no claims beyond its own session.
- It expires, it dies when the session ends, and the host can revoke it.
A display token is a key to a window, not a key to the house.
Format
Section titled “Format”d1.<base64url(payload)>.<hmac-sha256-hex>
payload = { sid, exp, ep, jti } — session id, expiry (epoch ms), the
session’s display epoch (see revocation), and a random id for logging.
The payload is signed, not encrypted: none of it is secret. Signing is what
stops a TV from editing sid and watching someone else’s party.
Revocation, without a new table
Section titled “Revocation, without a new table”The session record carries a displayEpoch counter. A token embeds the epoch
it was minted under; verification requires an exact match. Revoking every
outstanding token for a session is therefore a single increment — no
revocation list, no storage that can drift out of sync with reality, and no
way for a stale token to survive a bump.
Source: packages/slingshot-game-engine/src/lib/displayToken.ts
DisplayTokenVerification
Section titled “DisplayTokenVerification”Display tokens — casting a game to a real screen.
The problem
Section titled “The problem”Every game has a TV view: the big screen the room looks at. Until now none of them could actually be cast. Open the TV route on a Chromecast, a smart-TV browser, or any device that has never logged in, and every request 401s. It only appeared to work because the host opened the TV as a tab in their own already-authenticated browser, silently borrowing their session. In a music game where the TV is the speaker, that meant no TV and therefore no sound.
The threat model — say it out loud
Section titled “The threat model — say it out loud”A display token lives in a URL, on a screen, in somebody’s living room. Guests can read it off the TV. It will be photographed. Assume it leaks.
So a leaked display token must be a NON-EVENT. It grants exactly one thing: read-only visibility of ONE game session, exactly as a spectator would see it. It is not a login. It cannot be widened into one:
- The actor it produces has
kind: 'display'andid: null. Slingshot’suserAuthrequireskind === 'user'AND a non-nullid, so a display token can never satisfyuserAuth— on ANY route, in ANY package, present or future. Read-only is therefore structural, not a check somebody has to remember to write. Every app route that guards ongetActorId(c)already rejects it today, before those apps know display tokens exist. - It is bound to one
sessionId. It is useless against any other session. - It carries no user identity, no roles, and no claims beyond its own session.
- It expires, it dies when the session ends, and the host can revoke it.
A display token is a key to a window, not a key to the house.
Format
Section titled “Format”d1.<base64url(payload)>.<hmac-sha256-hex>
payload = { sid, exp, ep, jti } — session id, expiry (epoch ms), the
session’s display epoch (see revocation), and a random id for logging.
The payload is signed, not encrypted: none of it is secret. Signing is what
stops a TV from editing sid and watching someone else’s party.
Revocation, without a new table
Section titled “Revocation, without a new table”The session record carries a displayEpoch counter. A token embeds the epoch
it was minted under; verification requires an exact match. Revoking every
outstanding token for a session is therefore a single increment — no
revocation list, no storage that can drift out of sync with reality, and no
way for a stale token to survive a bump.
Source: packages/slingshot-game-engine/src/lib/displayToken.ts
GameErrorCodeValue
Section titled “GameErrorCodeValue”Game engine error codes and error class.
Every error the engine produces — REST and WS — uses a code from this registry. Codes are string constants that clients can switch on for i18n or UI logic. See spec §28.4 for the full registry.
Source: packages/slingshot-game-engine/src/errors.ts
HandlerFunction
Section titled “HandlerFunction”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
PhaseAdvanceTrigger
Section titled “PhaseAdvanceTrigger”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
RelayFilterFunction
Section titled “RelayFilterFunction”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ReplayEventType
Section titled “ReplayEventType”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
RoleVisibilityRule
Section titled “RoleVisibilityRule”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
ServerToClientMessage
Section titled “ServerToClientMessage”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
SessionStatus
Section titled “SessionStatus”All game domain model types.
Shared types live in this dedicated file (Rule 6). These types are
consumed by lib/, entities/, operations/, plugin.ts, and exported
via the ./types subpath for client SDK use.
Source: packages/slingshot-game-engine/src/types/models.ts
Exports
Section titled “Exports”defineGame
Section titled “defineGame”defineGame() DSL for game developers.
Accepts a GameDefinitionInput with partial/optional fields, applies
defaults, validates structure, and returns a frozen GameDefinition.
This is the primary API for game developers to define game types.
See spec §4 for the full contract.
Source: packages/slingshot-game-engine/src/defineGame.ts