# Game lifecycle on filudi.com

The frame, the handshake, the game token, the origin per game, the CSP, and what a game cannot do, in plain words.

A game on filudi.com is a static bundle in a sandboxed frame on a page of the hub. The SDK runs inside the
frame; the hub runs outside. Everything between them is a message. This page says what happens, in order,
and what it means for the code you write.

## Where the bundle lives

Every version is unpacked under `games/<slug>/<version>/` and served through the CDN from
`https://<slug>.<play domain>/<slug>/<version>/index.html`. One origin per game: your bundle shares nothing
with another game, not `localStorage`, not a cookie, not a cache. The host must match the path; the API
answers 404 for game A on the origin of game B. Until the play domain is switched on per game, the same
path is served from `https://play.filudi.com`.

The frame is `sandbox="allow-scripts allow-same-origin allow-pointer-lock"` with `allow="fullscreen;
autoplay"`. The page keeps the top-level navigation, the forms, the popups.

## The handshake

1. The hub opens the frame on the version's URL.
2. Your bundle calls `sdk.init({ gameId })`. The SDK posts `filudi:ready` to the parent and waits 1.5 s
   (`handshakeTimeoutMs`) for an answer.
3. The hub answers `filudi:init`: the API URL, a game token, the slug of the game, the player, the locale,
   the site, the age policy, the consent state, the version string. The SDK accepts it only from
   `window.parent`.
4. `init()` resolves. From here the SDK calls the API itself with the game token: saves, scores, events.
5. Your game draws its first frame and calls `sdk.ready()`. The SDK posts `filudi:loaded`; the hub hides
   its loader and counts the first step of your funnel.

No answer within 1.5 s means the bundle is not in a Filudi frame: the SDK runs on the mock and
`result.host` says so. That is what happens in the preview, which runs at the top level.

<CodePanel title="What the frame receives">
  <Fragment slot="javascript">
    ```js
    // filudi:init, posted by the hub. The SDK reads it; the game never does.
    ({
      type: 'filudi:init',
      apiUrl: 'https://api.filudi.com',
      token: 'fg1.…',                      // game token, opaque, 24 hours
      gameId: 'gloop',
      player: { id: '…', nickname: 'Guest-Fox', guest: true },
      locale: 'fr',
      site: 'filudi',
      policy: { personalizedAds: false, ageBand: 'all', accounts: true },
      consent: { ads: false, analytics: true },
      gameVersion: '1.4.0',
    });
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    import type { FiludiInit } from '@filudi/game-sdk';

    // filudi:init, posted by the hub. The SDK reads it; the game never does.
    const message: FiludiInit = {
      type: 'filudi:init',
      apiUrl: 'https://api.filudi.com',
      token: 'fg1.…',                      // game token, opaque, 24 hours
      gameId: 'gloop',
      player: { id: '…', nickname: 'Guest-Fox', guest: true },
      locale: 'fr',
      site: 'filudi',
      policy: { personalizedAds: false, ageBand: 'all', accounts: true },
      consent: { ads: false, analytics: true },
      gameVersion: '1.4.0',
    };
    ```

  </Fragment>
</CodePanel>

## The game token

The player signs in to filudi.com and has a token of their own. That token never enters your frame. The
hub exchanges it for a game token: limited to your game, to that player, for 24 hours. It opens your saves,
your boards and your events and nothing else. The hub sends a fresh one every 12 hours (`filudi:token`) and
the SDK uses it from the next request, without a reload.

When the API refuses the token, the SDK posts `filudi:unauthorized`, keeps saves in `localStorage` and
stops calling the API; the hub fetches a new token and reloads the frame. Your game sees a reload, nothing
else.

## While the game runs

The SDK posts to the hub: `filudi:gameplay { running }` on `gameplay.start()` and `stop()`, `filudi:save
{ version }` after a write, `filudi:score { board, score, rank }` after a submit, `filudi:track { event }`
after an event, `filudi:fullscreen { request: true }` on `sdk.fullscreen()`, `filudi:ad` to request an ad.
The hub treats each as a claim from an untrusted frame: it may update what the page shows for your game,
and nothing else.

The hub posts to the SDK: `filudi:consent`, `filudi:locale`, `filudi:pause`, `filudi:resume`,
`filudi:token`, and the answers to fullscreen and ad requests. They reach your game as the events of
[host events](/docs/sdk/events/), [i18n](/docs/sdk/i18n/) and [ads](/docs/sdk/ads/).

## The CSP

The bundle route sends a Content Security Policy. Scripts, styles, images, media, fonts and workers load from
the bundle's own origin only, with `blob:` and `data:` allowed for assets and `wasm-unsafe-eval` for
WebAssembly. `fetch`, XHR, WebSocket and EventSource reach the bundle's origin and the API only. No frame
inside the frame, no form post. An inline `<script>`, an `onclick=""` attribute, `eval`, `new Function` and
`setTimeout` with a string throw. Inline styles work.

<CodePanel title="The policy, as sent">
  <Fragment slot="javascript">
    ```text
    default-src 'self'; script-src 'self' 'wasm-unsafe-eval' blob:; style-src 'self' 'unsafe-inline';
    img-src 'self' data: blob:; media-src 'self' data: blob:; font-src 'self' data:;
    connect-src 'self' data: blob: https://api.filudi.com; worker-src 'self' blob:; frame-src 'none';
    object-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors <site origins>
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```text
    default-src 'self'; script-src 'self' 'wasm-unsafe-eval' blob:; style-src 'self' 'unsafe-inline';
    img-src 'self' data: blob:; media-src 'self' data: blob:; font-src 'self' data:;
    connect-src 'self' data: blob: https://api.filudi.com; worker-src 'self' blob:; frame-src 'none';
    object-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors <site origins>
    ```

  </Fragment>
</CodePanel>

What this rules out: a CDN library, Google Fonts, an analytics script, an error reporter, a call to your own
server. Put fonts, audio and data in the bundle. Report through `sdk.track()`. The portal scripts of
CrazyGames, Poki and Playgama never run on filudi.com; the SDK loads them on their own portals.

## What a game cannot do

- Read the player's own token, the page around it, or another game's storage.
- Navigate the page, open a popup, submit a form.
- Load code or assets from another origin, or call one.
- Submit a score outside a play session, or replay one: scores are signed with a key the session gives.
- Send an event named `frame_loaded`: the hub sends that one.
- Go live by itself: staff activates every version.

## Frame size and orientation

The frame takes the size the page gives it. Read `innerWidth` and `innerHeight` or a `ResizeObserver`, never
`screen.width`, and redraw on `resize`. The orientation you declare on the game (`portrait`, `landscape`,
`any`) tells the hub what to do on a phone held the other way: it shows a hint, your layout does not have to
cope. `mobile` says whether the game is listed on phones at all; `minutes` is the length of a typical
session, shown on the game's page.

Audio starts after a gesture: the frame allows `autoplay`, browsers still block sound before a tap. A
`pointerdown` handler that calls `preventDefault()` keeps the frame from taking keyboard focus; call
`canvas.focus()` in it, with `tabindex="0"` on the canvas, or the arrow keys scroll the hub page.
