# Harmony SDK

The Harmony SDK brings Harmony's accounts, friends and overlay into your game.

This is the plain-text version of https://playharmony.ai/developers/sdk, for AI coding agents and anyone who prefers Markdown. Both say the same; the notes for coding agents at the end are extra.

> **The SDK is in preview.** We add calls and options often during the preview. We keep each update compatible with existing games where we can, but an update may still change or remove a call your game uses. Install the latest version and keep it up to date.

## Included with every hosted game

When you upload a game, Harmony adds its overlay for you: a panel players open over your game with Shift+Tab. With no code at all, your players get:

- sign-in with their Harmony account, or play as a guest
- their friends, who is online and what they are playing
- chat with friends and other players while they play

Install the SDK when your game should know who is playing, show their friends, react to the overlay, or use the explicit `harmony.files` service. Once your game attaches to Harmony itself, Harmony sees it on upload and doesn't add a second overlay.

## Installing the SDK

Add the SDK to your game's project:

```sh
npm install @playharmony/sdk
```

Then call `attach` once as your game starts. A game with no build step loads the SDK with a script tag and calls `PlayHarmony.attach()` instead, as that section shows.

## How a hosted game runs

Your game runs in a frame on an address of its own, and the overlay runs in the page around it. From inside the game, the SDK reads the player's profile and friends list. Signing in, chat, friend requests and account changes happen in the overlay, where the player sees them.

Saves in `localStorage` or `IndexedDB` belong to your game alone: no other game can read them.

## Preparing your game for upload

Upload your game from My Creations, as one `.html` file or a `.zip` with its `index.html` at the top. Your upload limits are shown there. Most games need no changes; check these before you upload:

- Load your own files by relative addresses. Harmony serves your build under a folder of its own, not at the root of the site, so an address such as `/assets/game.js` doesn't load. With Vite, set `base: './'` in `vite.config.js`.
- Fullscreen works as usual: call `requestFullscreen` from a click or key handler, and Harmony makes the whole page fullscreen with the overlay still above your game.
- The overlay releases pointer lock when it opens. If your game needs it, lock the pointer again when the overlay closes, from `harmony.on("close")`.
- Downloads, `<input type="file">`, writing to the clipboard and `navigator.share` work as on your own page. Reading the clipboard doesn't.
- Native Open/edit/Save through `showOpenFilePicker` and `showSaveFilePicker` works only when Harmony's compatibility runtime is installed before game scripts and the host negotiates support. That basic workflow, including multiple-file Open and cancellation, needs no SDK attachment. Don't assume every hosted build has this runtime: automatic installation and migration remain separate rollout work. Otherwise use downloads and file inputs, or the explicit `harmony.files` service.
- Don't register a service worker: hosted games won't be able to. In Unity, use the Default or Minimal WebGL template, not PWA; in Godot, turn off Progressive Web App in the web export.

The basic picker support was released in 1.1.22 and has native evidence on Windows Chromium 153.0.8010.12. Other browsers and native Escape-key dismissal are not claimed as validated. Its handles are temporary: no IndexedDB persistence, structured clone, worker transfer, native handle branding or directory workflows. This does not restrict ordinary IndexedDB saves. Native Save selection can clear an existing file before returning a handle; abort does not restore its pre-selection bytes, and a failed close does not prove whether the write committed. The native facade and `harmony.files` have separate admission paths and handles or file IDs; don't interchange them. Existing asynchronous `attach()` and public aliases are unchanged.

## Showing text from Harmony

Players and creators write the text the SDK gives your game, such as names and file names. Harmony passes it on exactly as written: plain text, never HTML. Treat it as untrusted, and treat text that other players send your game, such as a name in a multiplayer message, the same way.

- Show it as plain text: with `textContent` on the page, or your canvas's or engine's text call, such as `fillText`. Where an engine reads markup in text, such as rich text in Unity's TextMeshPro or BBCode in Godot's RichTextLabel, turn that off for this text or escape it.
- Never turn it into HTML: not through `innerHTML`, `insertAdjacentHTML` or `document.write`, not through a framework's raw HTML, such as React's `dangerouslySetInnerHTML`, Vue's `v-html` or Svelte's `{@html}`, and not by building HTML from strings. Text such as `<img src=x onerror="…">` runs as code there, inside your game.
- Set an attribute through the element, as in `element.setAttribute("title", name)`, never by writing it into HTML.
- Never use it as a link or as code: not in an `href` or `src`, a CSS selector, `eval` or `new Function`.

List friends as text:

```ts
// friendList is your own element.
const friends = await harmony.friends.list();
for (const friend of friends) {
  const line = document.createElement("li");
  line.textContent = friend.displayName;
  line.title = friend.discriminator ? `${friend.displayName}#${friend.discriminator}` : friend.displayName;
  friendList.append(line);
}
```

## Testing your game

During development, your game connects to a stand-in for Harmony called the fake. It is the real SDK on a simulated backend, with bot friends and chat and no sign-in. It downloads like the real SDK, so you need a network connection. The bottom of the overlay's panel says `fake` so you can tell which one you're on. Your release build connects to the real Harmony.

- With the package, your bundler's development build picks the fake on its own: Vite's dev server, webpack's `--mode development`, esbuild with `--conditions=development`. Parcel needs package exports turned on, and Rollup the `development` export condition. `vite build` is a release build.
- With a script tag, load the fake loader in place of the real one, and swap it back when you ship:

  ```html
  <script src="https://cdn.playharmony.ai/v1/fake-loader.js"></script>
  ```

For automated tests in a DOM emulation such as jsdom, `loadFakeGameServices` downloads the fake into the test and returns its `attach`. Import it only from test files. A download can take up to 30 seconds, so raise the hook's timeout:

Attach to the fake in an automated test:

```ts
// Vitest with environment "jsdom".
import { beforeAll, expect, test } from "vitest";
import { loadFakeGameServices } from "@playharmony/sdk/testing";

let attachFake: Awaited<ReturnType<typeof loadFakeGameServices>>;

beforeAll(async () => {
  attachFake = await loadFakeGameServices();
}, 90_000);

test("the game attaches to Harmony", async () => {
  const container = document.createElement("div");
  document.body.append(container);
  const harmony = await attachFake({ gameId: "YOUR_GAME_ID", container });
  expect(harmony.isOpen()).toBe(false);
  harmony.detach();
});
```

No call reports the SDK's version. It is in your browser console on every attach, and in the overlay's About dialog, opened from the Harmony mark in its header.

## Theming the overlay

The overlay uses Harmony's light theme by default. Set these CSS custom properties on any ancestor of your game's container to change it. The panel and its confirmation cards sit on Harmony's art, so they keep their own text and surface colors; your accent, font, radius and the online and danger colors still apply there.

```css
:root {
  --mtc-accent: #ff5c8a;   /* names, links, icons, buttons */
  --mtc-surface: #171320;  /* menus, popups and dialogs */
  --mtc-text: #f2eef7;     /* the text on those */
  --mtc-backdrop: rgba(10, 6, 16, 0.65);
  --mtc-radius: 14px;
  --mtc-font: "Inter", system-ui, sans-serif;
  --mtc-online: #4ade80;   /* presence dots */
  --mtc-danger: #ff6b6b;   /* destructive actions, errors */
}
```

## attach

Every call starts with `attach`. It connects your game to Harmony, adds the overlay over your game's container, and gives back the handle the rest of this page calls `harmony`. Call it once, as your page loads. Hosted games and the fake can call `attach()` with no options.

Import the SDK before any code that changes the page's address, such as a client-side router: sign-in returns to the address your game started at.

### `attach(options?: AttachOptions): Promise<GameServicesHandle>`

Start your game without waiting for it: the overlay arrives when it is ready. In the window a player signs in through, the promise never settles, so never `await` it before your game starts. It rejects when Harmony can't start, with a `code`: `"sdk_unavailable"` when the SDK couldn't load, or `"invalid_params"` when an option is missing or wrong. Then your game plays on without Harmony's features.

Options:

- `gameId` (`string`): Optional. Hosted games get their Game ID from Harmony; the fake supplies a development ID.
- `container` (`HTMLElement`): Optional. Your canvas or connected wrapper element. Defaults to the document when omitted.
- `gameInfo` (`{ name?: string }`): Ignored in hosted games: Harmony uses the name from your game's listing metadata. In the fake, sets the name of the game chat.
- `hotkey` (`{ key, shift?, ctrl?, alt?, meta? } | null`): The key that opens and closes the overlay, matched against `KeyboardEvent.key`. Shift+Tab by default; `null` turns it off; the badge still works.
- `badgeCorner` (`"bottom-right" | "bottom-left" | "top-right" | "top-left"`): The corner of your game the overlay's badge sits in, with its panel beside it. By default the corner set for the player's device in the Harmony button panel on your game's listing page (on a desktop, once the player signs in), else top right on a phone and bottom right otherwise, following the layout as it changes; a corner you pick applies in every layout.

Attach to Harmony:

```ts
import { attach, type GameServicesHandle } from "@playharmony/sdk";

// Hosted games and the fake need no options. startGame is your own.
let harmony: GameServicesHandle | null = null;
attach().then(
  (handle) => {
    harmony = handle;
  },
  (error) => {
    // The game plays on without Harmony's features.
    console.warn("Harmony is not available", error);
  },
);

startGame();
```

A game with no build step loads Harmony with a script tag instead, and attaches through the `PlayHarmony` global. There, `PlayHarmony.attach` takes the same options and returns a promise of the handle. The loader makes the call available immediately. Use `.then()` to receive the handle and `.catch()` to handle a failure, while your game starts without waiting:

```html
<!-- First in <head>: -->
<script src="https://cdn.playharmony.ai/v1/loader.js"></script>

<!-- At the end of <body>, after the game's element exists. startGame is your own. -->
<script>
  // A sign-in window returning to this page finishes sign-in; it doesn't start the game.
  if (!window.__MANTICORE_AUTH_RETURN__) {
    startGame();
    if (window.PlayHarmony) {
      PlayHarmony.attach().then((harmony) => {
        // Read the session and subscribe to the overlay here.
      }).catch((error) => {
        console.warn("Harmony is not available", error);
      });
    }
  }
</script>
```

The handle and its `auth`, `friends` and `files` are frozen: keep the handle as it is, don't add fields to it, and keep it out of stores that wrap objects in deep proxies, such as Solid's `createStore` or MobX's `observable`. Every call can fail with the `code` `"invalid_params"`, `"not_connected"`, `"unknown_method"` or `"internal"`, besides the codes its own section names. Branch on the code, never on the message.

### `harmony.detach(): void`

Removes the overlay and every listener the SDK added, as when your game shuts down. After this, `getJoinUrl` still validates its arguments and returns `null` for valid calls.

Remove Harmony when leaving the game:

```ts
// showTitleScreen is your own.
function quitToTitle(): void {
  harmony.detach();
  showTitleScreen();
}
```

## Lobby invitations

Harmony does not currently provide a lobby service. This helper enables invitations for your own or an external lobby service by carrying its lobby code through Harmony's game launch flow. Your service creates and manages lobbies, admits players, and connects them to the game session.

### `harmony.getJoinUrl(options?: { params?: Readonly<Record<string, string>> }): Promise<string | null>`

Creates a website invitation for the attached game, using its identity and environment automatically. Supply your lobby code as `params.join`; no listing ID, hosting address or version path is needed. The helper makes no network request, opens no sign-in and does not navigate or copy the link.

Returns `null` in the fake, after detach, or without usable game and website context. Invalid arguments reject with `invalid_params`, even in the fake or after detach. Use an SDK release containing this call (1.1.10 or later) and a website with invitation forwarding enabled; an older website can drop the lobby code.

Prepare the URL when the lobby is ready, before the Copy Invite click, so the clipboard write starts directly in the click handler. Replace the sample lobby code with your own. Remove these controls when leaving the lobby and prepare fresh ones for the next lobby. If clipboard access is refused or unavailable, the selected URL remains available for manual copying.

Copy a lobby invitation:

```ts
const roomCode = "lobby-123";
const copyButton = document.createElement("button");
copyButton.type = "button";
copyButton.textContent = "Copy Invite";
copyButton.disabled = true;
const inviteField = document.createElement("input");
inviteField.readOnly = true;
inviteField.setAttribute("aria-label", "Invitation URL");
const inviteStatus = document.createElement("p");
inviteStatus.setAttribute("role", "status");
document.body.append(copyButton, inviteField, inviteStatus);

async function prepareInvite() {
  try {
    const url = await harmony.getJoinUrl({ params: { join: roomCode } });
    if (url === null) {
      inviteStatus.textContent = "Invitations are unavailable in this session.";
      return;
    }
    inviteField.value = url;
    copyButton.disabled = false;
  } catch (error) {
    const invalid = typeof error === "object" && error !== null
      && "code" in error && error.code === "invalid_params";
    inviteStatus.textContent = invalid
      ? "This lobby code cannot be included in an invitation."
      : "Could not prepare the invitation.";
  }
}

copyButton.addEventListener("click", async () => {
  try {
    await navigator.clipboard.writeText(inviteField.value);
    inviteStatus.textContent = "Invitation copied.";
  } catch {
    inviteField.focus();
    inviteField.select();
    inviteStatus.textContent = "Copy the selected invitation URL manually.";
  }
});

void prepareInvite();
```

Read the decoded `join` value inside the game before your router rewrites the address. `joinLobby` below is your own multiplayer code: validate the lobby and authorize the player there.

Read an invited lobby:

```ts
const incomingRoomCode = new URLSearchParams(window.location.search).get("join");
if (incomingRoomCode !== null) {
  joinLobby(incomingRoomCode);
}
```

Invitations do not grant access. Recipients still complete required sign-in and legal acceptance and must be allowed to play. Restricted games need separately authorized access; the helper never extracts sharing keys or copies the current page's query. Existing friend invitations keep their behavior.

Parameter names must be nonempty and values must be strings, with valid Unicode. Pass literal values without pre-encoding: the helper encodes spaces, plus signs, ampersands and Unicode. The limit is 32 pairs and 4096 bytes of encoded query, including separators. Names are case-sensitive. Reserved names are `mg:*`, `k`, `friend`, `t`, `code`, `state`, `error`, `error_description`, `error_uri` and `iss`.

## Overlay

The overlay is the Harmony panel over your game, with the player's friends, chat and inbox. Players open it with its badge or Shift+Tab; your game can open it too, and should pause while it is open.

> **While the overlay is open.** Your game gets no keyboard or pointer input: the SDK stops it before your listeners see it, and a click on the game closes the overlay. A key the player was holding still gets its `keyup`. The one exception is a capture listener on `window` that your game added before attaching: it should return early while `harmony.isOpen()`, and not cancel keys, or typing in the overlay loses characters.

### `harmony.open(): void`

Opens the overlay over your game, where a player who isn't signed in is offered sign-in. Does nothing while it is already open.

Open the overlay from your own button:

```ts
// friendsButton is your own.
friendsButton.addEventListener("click", () => {
  harmony.open();
});
```

### `harmony.close(): void`

Closes the overlay and returns focus to your game.

Close the overlay when a round starts:

```ts
// game is your own.
function startRound(): void {
  harmony.close();
  game.resume();
}
```

### `harmony.toggle(): void`

Opens the overlay when it is closed, and closes it when it is open.

Open and close the overlay with another key:

```ts
window.addEventListener("keydown", (event) => {
  if (event.key === "F2") {
    harmony.toggle();
  }
});
```

### `harmony.isOpen(): boolean`

Whether the overlay is open now.

Close the overlay before your own pause menu:

```ts
// openPauseMenu is your own.
function onPauseKey(): void {
  if (harmony.isOpen()) {
    harmony.close();
  } else {
    openPauseMenu();
  }
}
```

### `harmony.on(event: "open" | "close", listener: () => void): () => void`

Calls your listener each time the overlay opens or closes, however it happened. It returns a function that stops listening.

Pause while the overlay is open:

```ts
// game is your own.
const stopOpen = harmony.on("open", () => {
  game.pause();
});
const stopClose = harmony.on("close", () => {
  game.resume();
});

// Later, to stop listening:
stopOpen();
stopClose();
```

## auth

Who is playing. Players sign in, or play as a guest, through the overlay; your game reads the result. A guest who later creates an account keeps the same `accountId`.

> **About names.** A display name is 3 to 20 characters of letters, digits, `.`, `_` and `-`, and starts and ends with a letter or digit. A tag is 4 characters. Two players can share a name, so show the tag beside it where telling them apart matters, and key on `accountId`.

> **Harmony doesn't store game data.** Keep progress in the browser, in `localStorage` or `IndexedDB`, keyed on the player's `accountId` so each player on a device has their own. It stays on that device and in that browser: it doesn't follow the player to another one. For saves on your own server, authenticate requests with `auth.getBackendToken()` and use the verified identity there.

### `harmony.auth.getSession(): Promise<PlayerSession>`

The player's session now. Its `kind` is `"account"` for a Harmony account, `"guest"` for a guest, `"signedOut"`, or `"legalRequired"` while the player accepts updated terms in the overlay. Treat any kind but the first two as not signed in. A guest or account session also has:

- `accountId` (`string`): The player's id, the same in every session. Key the saves you keep in the browser on this.
- `displayName` (`string`): The name to show: 3 to 20 letters, digits, dots, underscores and hyphens. Not unique: two players can share it, so never key anything on it. Show it as text, as in Showing text from Harmony.
- `discriminator` (`string | null`): The 4-character tag that tells players with the same name apart. It is never part of displayName.

Show who is playing:

```ts
// nameLabel is your own element.
const session = await harmony.auth.getSession();
if (session.kind === "guest" || session.kind === "account") {
  nameLabel.textContent = session.displayName;
} else {
  nameLabel.textContent = "Not signed in";
}
```

### `harmony.auth.getBackendToken(): Promise<BackendToken>`

Proves the current guest or account to your game's own server, in a Harmony-hosted frame or on your own page. Returns:

- `token` (`string`): The signed identity token.
- `expiresAtMs` (`number`): Expiration in Unix epoch milliseconds.

It never opens sign-in. It rejects with `not_signed_in` while signed out or awaiting legal acceptance, or `unsupported` on a backend without this feature.

On your server, verify the RS256 signature using the Harmony environment's trusted JWKS signing keys. Require the configured issuer, your game's audience `urn:ugs:game-backend:<gameId>`, header type `ugs-backend-identity+jwt`, and a valid expiry. Configure the issuer and JWKS address on the server; never trust addresses supplied by the token. Use the verified `sub` as the save owner's account ID. A browser-supplied account ID, including one from `getSession()`, cannot authenticate a request.

Tokens last at most five minutes and can expire sooner. They remain usable until expiry after sign-out or revocation. Request one before a server request, send it over HTTPS, and keep it out of URLs, logs and persistent storage. It grants no Game Services API permissions. Your server verifies it locally; no token-validation API call is needed.

The fake SDK uses a public development signing key and a separate `.invalid` issuer. Accept fake tokens only with an explicitly configured local-development verifier, never against production data.

Authenticate a request to your own server:

```ts
const { token } = await harmony.auth.getBackendToken();
const response = await fetch("https://your-server.example/saves", {
  headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
  throw new Error("Loading saves failed");
}
// The server verifies the token and chooses the save owner from its verified sub.
showProgress(await response.json());
```

### `harmony.auth.signIn(): Promise<PlayerSession>`

Starts sign-in in the overlay and resolves with the session it ends in. It may open a window, so call it from the player's click or key press.

Offer sign-in from your own button:

```ts
// signInButton is your own.
signInButton.addEventListener("click", async () => {
  const session = await harmony.auth.signIn();
  if (session.kind === "account" || session.kind === "guest") {
    signInButton.hidden = true;
  }
});
```

### `harmony.auth.onSessionChanged(listener: (session: PlayerSession) => void): () => void`

Calls your listener with the whole new session each time it changes: the player signs in, signs out, or a guest creates an account. It returns a function that stops listening.

Load the saved progress of whoever signs in:

```ts
// showProgress is your own. Progress is kept in this browser, one entry per player.
const stopWatching = harmony.auth.onSessionChanged((session) => {
  if (session.kind === "guest" || session.kind === "account") {
    const saved = localStorage.getItem(`progress:${session.accountId}`);
    showProgress(saved === null ? null : JSON.parse(saved));
  }
});
```

## friends

The player's friends, whether they are online, and whether they are playing your game. Friend requests and changes to the list happen in the overlay. A guest has no friends list, so check that the session's `kind` is `"account"` before you ask for one.

### `harmony.friends.list(): Promise<Friend[]>`

The player's friends. It rejects with the `code` `"guest_not_allowed"` for a guest, and `"not_signed_in"` when no one is signed in; show your game without friends then. Each friend has:

- `accountId` (`string`): The friend's id.
- `displayName` (`string`): The name to show, as for the player. Not unique.
- `discriminator` (`string | null`): The 4-character tag beside the name.
- `online` (`boolean`): Whether the friend is online now.
- `playingSameGame` (`boolean`): Whether the friend is visibly playing your authenticated game.

Count the friends in this game:

```ts
// statusLabel is your own element.
const friends = await harmony.friends.list();
const here = friends.filter((friend) => {
  return friend.playingSameGame;
});
statusLabel.textContent = `${here.length} friends in this game`;
```

### `harmony.friends.onPresenceChanged(listener: (friend: Friend) => void): () => void`

Calls your listener with a friend's whole, current entry each time they come online, go offline or change game. It returns a function that stops listening.

Keep your friend list's online dots current:

```ts
// friendItems is your own Map of each friend's accountId to their list item.
const stopWatching = harmony.friends.onPresenceChanged((friend) => {
  friendItems.get(friend.accountId)?.classList.toggle("online", friend.online);
});
```

## files

Real Save, Save As and Open dialogs for files on the player's device. Browsers don't allow file dialogs inside a hosted game's frame, so Harmony shows them for you. Keep progress in `localStorage` or `IndexedDB`; use files for things players keep or share, such as levels, drawings or exported saves.

Call both from a click or key handler: browsers show file dialogs only while the player is acting. Called at another time, a first save downloads the file instead, and an open fails. Firefox and Safari have no save dialog, so there a save downloads the file.

### `harmony.files.save(options: SaveFileOptions): Promise<SaveFileResult>`

Options:

- `data` (`Blob | ArrayBuffer | typed array`): What to write. Required.
- `suggestedName` (`string`): The name the dialog, or the download, starts with.
- `types` (`{ description?, accept }[]`): The file types to offer, as MIME types with their extensions, such as `{ "application/json": [".json"] }`.
- `fileId` (`string | null`): An id from an earlier save or open: writes that file again without asking where, so autosave works.

The result's `outcome` is `"saved"` with a `fileId` to save to again, `"downloaded"` with no id, or `"cancelled"` when the player closed the dialog.

Save a level, then save to the same file again:

```ts
// saveButton and level are your own.
let fileId: string | null = null;

saveButton.addEventListener("click", async () => {
  const result = await harmony.files.save({
    data: new Blob([JSON.stringify(level)], { type: "application/json" }),
    suggestedName: "level.json",
    types: [{ description: "Level", accept: { "application/json": [".json"] } }],
    fileId,
  });
  if (result.outcome !== "cancelled") {
    fileId = result.fileId;
  }
});
```

### `harmony.files.open(options?: OpenFileOptions): Promise<OpenFileResult>`

Shows an Open dialog, limited to the `types` you pass. The result's `outcome` is `"opened"`, with the file's `data` as an `ArrayBuffer`, its `name` and a `fileId` to save back to it, or `"cancelled"`.

The player can pick any file, so check its `data` before you use it, and show its `name` as text: a file name can hold any character.

Open a level, checking it first:

```ts
// openButton, level, isLevel and showToast are your own. The player can pick any file, so check it before use.
openButton.addEventListener("click", async () => {
  const result = await harmony.files.open({
    types: [{ description: "Level", accept: { "application/json": [".json"] } }],
  });
  if (result.outcome === "opened") {
    try {
      const parsed: unknown = JSON.parse(new TextDecoder().decode(result.data));
      if (isLevel(parsed)) {
        level = parsed;
      } else {
        showToast("That file isn't a level.");
      }
    } catch {
      showToast("That file isn't a level.");
    }
  }
});
```

The player closing a dialog isn't an error: it answers `"cancelled"`. A save or open that can't go ahead rejects with a `code`:

- `"no_user_activation"`: it wasn't called from the player's click or key press, or the player was busy with the overlay. Try again from a click.
- `"busy"`: another save or open is still waiting on the player.
- `"file_not_found"`: the `fileId` isn't from this visit, or the file was moved or deleted. Ask where to save again.
- `"file_too_large"`: the file is bigger than a save or open allows.
- `"invalid_params"`: an option is missing or wrong.
- `"internal"`: the browser couldn't read or write the file, start the download, or show the dialog.
- `"not_connected"`: the call came after `harmony.detach()`.

## Notes for coding agents

- Use only the calls on this page; anything else is internal and can change in any release. Friend requests, chat and account changes happen in the overlay: open it with `harmony.open()` rather than building screens for them.
- Hosted games and the fake can call `attach()` without options.
- A guest has no friends list: check that `session.kind` is `"account"` before calling `harmony.friends.list()`, and show the game without friends otherwise.
- Attach with `attach` from the package, or `PlayHarmony.attach` from a script tag. Start the game without waiting for it, and never `await` it at the top level.
- Every call can fail: wrap them so the game keeps playing without Harmony's features, and branch on `error.code`.
- When supplying a container, use a connected wrapper or canvas, never `document.body`. Keep the handle as it is: it is frozen.
- Develop and test against the fake, as in Testing your game.
- Show every name, title and file name from Harmony as text, as in Showing text from Harmony.
- Pause the game while the overlay is open, with `harmony.on("open")` and `harmony.on("close")`.
- Install the latest SDK version, and update it when a new one is released.
