/** * Portals SDK v1.10.1 * * Escape is a reserved platform key: when the player presses it inside a * hosted game, the SDK notifies the host page, which opens the trusted * Portals in-game menu. Games still receive the keydown and may run their * own pause UI alongside it, but cannot suppress the platform menu. * * The runtime is injected into hosted games at ./_portals/sdk.js. * Reference this file from a TypeScript project or use it for editor * autocomplete. Games must not bundle credentials or call Portals private * APIs directly. */ export type PortalsContext = "standalone" | "room"; export interface PortalsPlayer { /** Stable within this game and deliberately unlinkable across games. */ playerId: string | null; displayName: string | null; avatarUrl: string | null; } export type PortalsPlayerAvatarBodyType = "male" | "female"; export type PortalsPlayerAvatarSlot = | "hat" | "glasses" | "neck" | "top" | "sleeves" | "gloves" | "left_hand" | "right_hand" | "back" | "bottom" | "shoes" | "full_body" | "not_interact"; export interface PortalsPlayerAvatarWearable { /** Canonical shop listing id used by the managed item proxy. */ itemId: string; name: string; slots: PortalsPlayerAvatarSlot[]; glb: string; removesHair: boolean; } export interface PortalsPlayerFullAvatar { /** Canonical shop listing id used by the managed item proxy. */ itemId: string; name: string; glb: string; } /** Sanitized, render-only form of the active look saved on `/avatar`. */ export interface PortalsPlayerAvatar { config: { bodyType: PortalsPlayerAvatarBodyType; skinColor: string; hairColor: string; hairStyle: string; }; customAvatar: PortalsPlayerFullAvatar | null; wearables: PortalsPlayerAvatarWearable[]; } /** Current public profile and playable avatar, read on demand. */ export interface PortalsPlayerProfile extends PortalsPlayer { /** Public profile handle (`publicProfileKey`), without `@`. */ username: string | null; /** Current `/avatar` presentation; null for guests or when unavailable. */ avatar: PortalsPlayerAvatar | null; } export interface PortalsPlayerApi { /** Reads the latest public username and `/avatar` look from the Portals host. */ get(): Promise; } export interface PortalsAvatarApi { /** * Opens trusted Portals UI for the player's global `/avatar` look. * Call from a player action. The game never receives raw inventory or * direct item mutation authority. */ openPicker(): Promise; } export interface PortalsSession { player: PortalsPlayer; context: PortalsContext; matchmaking: PortalsMatchmakingContext | null; } /** Opaque hosted-match state. Portals intentionally owns server identity. */ export interface PortalsMatchmakingContext { managed: true; visibility: "public" | "private"; phase: "starting" | "in_progress"; region: string; } export interface PortalsMatchmaking { /** Returns the current hosted-match context, or null outside matchmaking. */ current(): Promise; /** Subscribes to hosted-match lifecycle changes. Returns an unsubscribe. */ onChange( listener: (matchmaking: PortalsMatchmakingContext | null) => void ): () => void; } export interface PortalsLeaderboardOptions { /** Lowercase letters, numbers, or hyphens. Omit for "default". */ mode?: string; /** Number of top entries to return. Defaults to 10 and may not exceed 100. */ limit?: number; } export interface PortalsLeaderboardEntry { rank: number; /** A stable game-scoped player id, never the player's Portals account id. */ playerId: string; displayName: string | null; avatarUrl: string | null; score: number; } export interface PortalsLeaderboard { mode: string; entries: PortalsLeaderboardEntry[]; } export interface PortalsIdentity { /** * Opens Portals sign-in when needed. Call from a direct player action so a * browser popup or modal is not blocked. */ requestLogin(): Promise; /** Subscribes to sign-in and sign-out changes. Returns an unsubscribe. */ onChange(listener: (player: PortalsPlayer) => void): () => void; } export interface PortalsNetPlayer { /** Per-connection id (two tabs are two players); `from` in "message". */ id: string; /** Game-scoped player id, never the Portals account id; null for guests. */ playerId: string | null; displayName: string | null; avatarUrl: string | null; } export interface PortalsNetJoinOptions { /** * Optional sub-lobby within the host-chosen session bucket. Fast-paced * games are split by region; a `global:` prefix shares one worldwide * room instead (higher latency for players far from it). */ channel?: string; /** * Forces every player of this session onto Portals' US or EU servers * instead of the one nearest each of them. Use it when a game needs one * worldwide room in a known place; leave it off otherwise, since players * far from the pinned region pay the latency. `join()` rejects when the * pinned region has no capacity — a pin never silently falls back. */ region?: "us" | "eu"; } export interface PortalsNetSession { self: PortalsNetPlayer; players: PortalsNetPlayer[]; /** Shared last-write-wins key-value state snapshot at join time. */ state: Record; } export interface PortalsNetEvents { /** A broadcast from another player (see send()). */ message: (data: unknown, fromId: string) => void; playerjoin: (player: PortalsNetPlayer, players: PortalsNetPlayer[]) => void; playerleave: (player: PortalsNetPlayer, players: PortalsNetPlayer[]) => void; /** Confirmed shared-state write — fires for the writer too. */ state: (key: string, value: unknown) => void; /** "disconnected" on connection loss; rejoin with join(). */ status: (status: "connected" | "disconnected") => void; } export interface PortalsNet { /** * Joins the multiplayer session for this game. Rejects when multiplayer is * unavailable (no host, older host, connection failure) — keep a playable * solo fallback. */ join(options?: PortalsNetJoinOptions): Promise; leave(): Promise; /** Broadcasts at most 8 KB of JSON to the other players in the session. */ send(data: unknown): void; /** * Writes shared last-write-wins state (value at most 8 KB of JSON, at most * 64 keys, key at most 128 chars) that late joiners receive in join(). */ setState(key: string, value: unknown): void; getState(key: string): unknown; getState(): Record; players(): PortalsNetPlayer[]; self(): PortalsNetPlayer | null; on( event: E, handler: PortalsNetEvents[E] ): void; off( event: E, handler: PortalsNetEvents[E] ): void; } export interface PortalsVoiceJoinOptions { /** Optional sub-lobby within the host-chosen session bucket. */ channel?: string; } export interface PortalsVoiceSession { self: PortalsNetPlayer; participants: PortalsNetPlayer[]; muted: boolean; } export interface PortalsVoiceDevice { /** Pass to setDevice() to capture from this microphone. */ deviceId: string; /** * The browser's name for the device. Empty until the player has granted * the microphone, so render a fallback like "microphone 1". */ label: string; /** * The microphone Portals captures from. All entries are false while Portals * is still on the browser default. */ active: boolean; } export interface PortalsVoiceEvents { participantjoin: ( participant: PortalsNetPlayer, participants: PortalsNetPlayer[] ) => void; participantleave: ( participant: PortalsNetPlayer, participants: PortalsNetPlayer[] ) => void; /** Ids (PortalsNetPlayer.id) currently speaking, self included. */ speaking: (speakingIds: string[]) => void; /** "disconnected" on connection loss; rejoin with join(). */ status: (status: "connected" | "disconnected") => void; } export interface PortalsVoice { /** * Joins voice chat for this game's session. The Portals host owns the * microphone and the connection — the game never accesses the mic. Safe to * call on startup: the host asks the player for the microphone itself, so * the game needs no join button. Expect rejection (no host, the player * declined, connection failure) — keep the game fully playable without * voice, and offer a control that calls join() again. */ join(options?: PortalsVoiceJoinOptions): Promise; leave(): Promise; /** Mutes or unmutes the local microphone within the joined session. */ setMuted(muted: boolean): void; muted(): boolean; /** * Lists the microphones Portals can capture from, for an in-game mic * picker. Works before and after join(); labels are only filled in once * the player has granted the microphone. Rejects wherever voice chat * itself is unavailable. */ devices(): Promise; /** * Captures from another microphone. Applies immediately in a joined session * and is remembered for the next join(). Rejects when the device cannot be * used — keep the previously selected entry shown in that case. */ setDevice(deviceId: string): Promise; participants(): PortalsNetPlayer[]; self(): PortalsNetPlayer | null; on( event: E, handler: PortalsVoiceEvents[E] ): void; off( event: E, handler: PortalsVoiceEvents[E] ): void; } export interface PortalsEconomyProduct { sku: string; title: string; description: string; kind: "durable" | "consumable"; coinPrice: number; grantQuantity: number; purchaseLimitPerPlayer: number | null; /** Maximum completed purchases across every player, or null when uncapped. */ purchaseLimitTotal: number | null; /** Current effective purchase count, or null while historical availability is unavailable. */ purchaseCountTotal: number | null; purchasesRemaining: number | null; soldOut: boolean | null; /** Release-relative image path. Rendering remains subject to the game CSP. */ iconPath: string | null; } export interface PortalsEconomyInventoryItem { sku: string; kind: "durable" | "consumable"; quantity: number; status: "active" | "revoked"; purchaseCount: number; } export interface PortalsEconomyPurchaseResult { status: "purchased" | "cancelled"; sku: string; receiptId: string | null; quantity: number | null; } export interface PortalsEconomyConsumeResult { sku: string; /** Remaining aggregate quantity after this idempotent operation. */ quantity: number; idempotent: boolean; } export interface PortalsEconomy { /** Exact immutable product catalog bundled with the current game release. */ getCatalog(): Promise; /** Signed-in player's inventory for this game. */ getInventory(): Promise; /** * Opens Portals-owned confirmation UI. Must be called directly from a * player click or tap; game code can select only a released SKU. */ purchase(sku: string): Promise; /** * Consumes a granted quantity. Reuse the same stable operationId when * retrying the same gameplay event so it cannot consume twice. */ consume( sku: string, quantity: number, operationId: string ): Promise; } export interface PortalsSdk { readonly version: string; ready(): Promise; getPlayer(): Promise; /** On-demand current public username and playable `/avatar` look. */ readonly player: PortalsPlayerApi; readonly avatar: PortalsAvatarApi; readonly identity: PortalsIdentity; readonly matchmaking: PortalsMatchmaking; /** Saves at most 64 KB of JSON-serializable state for the signed-in player. */ saveState(data: unknown): Promise; /** Returns the signed-in player's saved state, or null when none exists. */ loadState(): Promise; /** * Records the signed-in player's best casual score. Higher scores win. * Client-reported scores must never control prizes, currency, or access. */ submitScore(score: number, mode?: string): Promise; /** Reads the top casual scores after the host enforces game access. */ getLeaderboard( options?: PortalsLeaderboardOptions ): Promise; /** Released products, player inventory, purchases, and consumable use. */ readonly economy: PortalsEconomy; /** Requests that the current Portals host close this game. */ quit(): void; /** Real-time multiplayer relay for the players of this game. */ readonly net: PortalsNet; /** Voice chat with the players of this game's session. */ readonly voice: PortalsVoice; } /** * Local development only. Declared on window BEFORE the sdk.js script tag, * this enables real multiplayer for a game served outside Portals (e.g. a * local vite server): net.join() authorizes against the Portals API with the * dev token and connects to the multiplayer fleet directly. Local sessions * join a "dev"-namespaced channel, never live players. The token expires * after a few hours — treat it like a password and never commit or ship it. */ export interface PortalsDevConfig { /** Temporary dev token from POST /api/v2/arcade/dev-token (owner-only). */ token: string; /** Portals API origin. Defaults to https://portals.to */ apiBase?: string; } declare global { const Portals: PortalsSdk; interface Window { Portals: PortalsSdk; __PORTALS_DEV__?: PortalsDevConfig; } }