Skip to content

@lastshotlabs/slingshot-game-engine

npm install @lastshotlabs/slingshot-game-engine

Display tokens — casting a game to a real screen.

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.

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' and id: null. Slingshot’s userAuth requires kind === 'user' AND a non-null id, so a display token can never satisfy userAuth — 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 on getActorId(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.

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.

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,): DisplayTokenVerification

Source: packages/slingshot-game-engine/src/lib/displayToken.ts

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): void

Source: packages/slingshot-game-engine/src/middleware/contentValidationGuard.ts

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): void

Source: packages/slingshot-game-engine/src/middleware/lobbyOnlyGuard.ts

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): void

Source: packages/slingshot-game-engine/src/middleware/rulesValidationGuard.ts

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[] } = {},): SlingshotPackageDefinition

Source: packages/slingshot-game-engine/src/plugin.ts

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(): void

Source: packages/slingshot-game-engine/src/policy/index.ts

Display tokens — casting a game to a real screen.

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.

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' and id: null. Slingshot’s userAuth requires kind === 'user' AND a non-null id, so a display token can never satisfy userAuth — 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 on getActorId(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.

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.

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

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 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

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

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 | null

Source: packages/slingshot-game-engine/src/lib/displayRuntime.ts

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): string

Source: packages/slingshot-game-engine/src/lib/display.ts

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): boolean

Source: packages/slingshot-game-engine/src/lib/displayRuntime.ts

Display tokens — casting a game to a real screen.

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.

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' and id: null. Slingshot’s userAuth requires kind === 'user' AND a non-null id, so a display token can never satisfy userAuth — 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 on getActorId(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.

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.

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; }): void

Source: packages/slingshot-game-engine/src/lib/displayToken.ts

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): string

Source: packages/slingshot-game-engine/src/lib/display.ts

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>): void

Source: packages/slingshot-game-engine/src/policy/index.ts

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): string

Source: packages/slingshot-game-engine/src/lib/display.ts

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): string

Source: packages/slingshot-game-engine/src/lib/display.ts

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): string

Source: packages/slingshot-game-engine/src/lib/display.ts

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): string

Source: packages/slingshot-game-engine/src/lib/display.ts

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): string

Source: packages/slingshot-game-engine/src/lib/display.ts

Display tokens — casting a game to a real screen.

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.

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' and id: null. Slingshot’s userAuth requires kind === 'user' AND a non-null id, so a display token can never satisfy userAuth — 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 on getActorId(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.

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.

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 },): DisplayTokenVerification

Source: packages/slingshot-game-engine/src/lib/displayToken.ts

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

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

Zod validation schema for GameEnginePluginConfig.

Validated at plugin construction time via validatePluginConfig() and frozen via deepFreeze() (Rule 10).

FieldDescription
cleanupCleanup configuration for completed/abandoned sessions.
disableRoutesRoutes to disable. Keys are entityName.operationOrAction strings.
disconnectDefault disconnect configuration.
heartbeatWS heartbeat configuration.
mountPathMount path for game engine REST routes. Default: /game.
recoveryWS message persistence and recovery configuration.
wsEndpointWS endpoint name. Default: ‘game’.
wsRateLimitWS rate-limiting configuration (per-socket, rolling window).

Source: packages/slingshot-game-engine/src/validation/config.ts

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

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 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

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

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 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

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

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

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

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

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

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

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

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

Display tokens — casting a game to a real screen.

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.

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' and id: null. Slingshot’s userAuth requires kind === 'user' AND a non-null id, so a display token can never satisfy userAuth — 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 on getActorId(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.

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.

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

Display tokens — casting a game to a real screen.

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.

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' and id: null. Slingshot’s userAuth requires kind === 'user' AND a non-null id, so a display token can never satisfy userAuth — 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 on getActorId(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.

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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Display tokens — casting a game to a real screen.

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.

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' and id: null. Slingshot’s userAuth requires kind === 'user' AND a non-null id, so a display token can never satisfy userAuth — 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 on getActorId(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.

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.

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

Display tokens — casting a game to a real screen.

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.

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' and id: null. Slingshot’s userAuth requires kind === 'user' AND a non-null id, so a display token can never satisfy userAuth — 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 on getActorId(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.

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.

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 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

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

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

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

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

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

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

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

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