# Мост

Мост — единственный выход приложения из песочницы. Объект `window.pairx` появляется до первого скрипта страницы; его нельзя подменить.

| Свойство | Что это |
| --- | --- |
| `pairx.version` | Версия моста, сейчас `"1.0"`. |
| `pairx.call(method, params?)` | Вызов метода; `Promise` с результатом или ошибкой. Вызов вместе с параметрами — до 64 КиБ JSON. |
| `pairx.on(event, callback)` | Подписка на событие; возвращает функцию, которая отписывает. |
| `pairx.launch` | Параметры запуска — неизменяемый объект, см. [ниже](#launch). |

```js
try {
  const { id } = await pairx.call("user.get");
} catch (error) {
  console.log(error.code, error.message);   // "unsupported", "offline", …
}
const off = pairx.on("storage.changed", ({ changes }) => { /* … */ });
off();
```

Имя метода — сегменты через точку, каждый начинается со строчной латинской буквы: `ui.mainButton`, `storage.write`. Неизвестный метод отклоняется с кодом `unsupported`; проверить заранее — `app.supports`.

## Методы {#methods}

| Метод | Что делает |
| --- | --- |
| [`app.ready`](#app.ready) | Интерфейс готов: Pairx убирает заставку. |
| [`app.close`](#app.close) | Закрыть приложение. |
| [`app.supports`](#app.supports) | Есть ли метод в этой версии Pairx — для возможностей, которые появляются позже. |
| [`user.get`](#user.get) | Кто открыл приложение. `id` — человек внутри этого приложения: в другом приложении у него другой ID; по нему нельзя узнать аккаунт Pairx. |
| [`context.get`](#context.get) | Где открыто приложение: общий экземпляр в чате, личный контекст или предпросмотр автора перед отправкой. |
| [`ui.theme`](#ui.theme) | Тема Pairx: светлая или тёмная и её цвета. |
| [`ui.safeArea`](#ui.safeArea) | Отступы от краёв, которые не закрывает Pairx (шапку и нативную кнопку Pairx рисует сам, их отступы уже учтены). |
| [`ui.mainButton`](#ui.mainButton) | Нативная кнопка Pairx внизу экрана. |
| [`ui.backButton`](#ui.backButton) | «Назад» в шапке Pairx вместо «Закрыть» — для своих экранов приложения; нажатие приходит событием `ui.backButton.clicked`. |
| [`ui.popup`](#ui.popup) | Нативное окно; в заголовке Pairx пишет название приложения и «код пользователя», чтобы окно нельзя было выдать за окно Pairx. |
| [`ui.toast`](#ui.toast) | Короткая подсказка внизу экрана. |
| [`storage.snapshot`](#storage.snapshot) | Всё состояние общих данных на одном `seq` — см. «Общие данные». |
| [`storage.write`](#storage.write) | Одна запись в общее хранилище; операции — в справочнике хранилища. |
| [`storage.private.set`](#storage.private.set) | Личные данные человека. |
| [`clipboard.writeText`](#clipboard.writeText) | Скопировать текст, до 10 000 символов; только по нажатию человека. |
| [`diagnostics.error`](#diagnostics.error) | Сообщить об ошибке приложения. |

### `app.ready` {#app.ready}

Интерфейс готов: Pairx убирает заставку. Вызывайте сразу, как нарисован первый экран.

Результат: `true`

### `app.close` {#app.close}

Закрыть приложение.

Результат: `true`

### `app.supports` {#app.supports}

Есть ли метод в этой версии Pairx — для возможностей, которые появляются позже.

| Параметр | Тип | Что |
| --- | --- | --- |
| `name` | `string` | Имя метода. |

Результат: `boolean`

```js
if (await pairx.call('app.supports', { name: 'share.toChat' })) { /* … */ }
```

### `user.get` {#user.get}

Кто открыл приложение. `id` — человек внутри этого приложения: в другом приложении у него другой ID; по нему нельзя узнать аккаунт Pairx.

Результат: `{ id: string, locale: string }`

```js
const { id } = await pairx.call('user.get');
```

### `context.get` {#context.get}

Где открыто приложение: общий экземпляр в чате, личный контекст или предпросмотр автора перед отправкой.

Результат: `{ kind: "chat" | "personal" | "preview", preview: boolean }`

### `ui.theme` {#ui.theme}

Тема Pairx: светлая или тёмная и её цвета. CSS `prefers-color-scheme` в приложении следует той же теме.

Результат: `{ scheme: "light" | "dark", colors: { accent, background, surface, text, muted } }`

### `ui.safeArea` {#ui.safeArea}

Отступы от краёв, которые не закрывает Pairx (шапку и нативную кнопку Pairx рисует сам, их отступы уже учтены).

Результат: `{ top, bottom, left, right }`

### `ui.mainButton` {#ui.mainButton}

Нативная кнопка Pairx внизу экрана. Нажатие приходит событием `ui.mainButton.clicked`.

| Параметр | Тип | Что |
| --- | --- | --- |
| `text` | `string` | Надпись, до 40 символов; пустая прячет кнопку. |
| `visible` | `boolean?` | Показать (по умолчанию true) или спрятать. |
| `enabled` | `boolean?` | Доступна ли (по умолчанию true). |
| `progress` | `boolean?` | Индикатор вместо надписи, пока идёт запись. |

Результат: `true`

```js
await pairx.call('ui.mainButton', { text: 'Отправить', visible: true });
pairx.on('ui.mainButton.clicked', send);
```

### `ui.backButton` {#ui.backButton}

«Назад» в шапке Pairx вместо «Закрыть» — для своих экранов приложения; нажатие приходит событием `ui.backButton.clicked`.

| Параметр | Тип | Что |
| --- | --- | --- |
| `visible` | `boolean` | Показать «Назад» (true) или вернуть «Закрыть». |

Результат: `true`

### `ui.popup` {#ui.popup}

Нативное окно; в заголовке Pairx пишет название приложения и «код пользователя», чтобы окно нельзя было выдать за окно Pairx. До трёх кнопок; ответ — `id` нажатой.

| Параметр | Тип | Что |
| --- | --- | --- |
| `message` | `string` | Текст, до 500 символов. |
| `buttons` | `{ id, text, type? }[]?` | Кнопки, надпись до 40 символов; `type` — `cancel` или `destructive`. Без кнопок — одна «OK». |

Результат: `string`

```js
const choice = await pairx.call('ui.popup', {
  message: 'Удалить запись?',
  buttons: [{ id: 'no', text: 'Отмена', type: 'cancel' }, { id: 'yes', text: 'Удалить', type: 'destructive' }],
});
```

### `ui.toast` {#ui.toast}

Короткая подсказка внизу экрана.

| Параметр | Тип | Что |
| --- | --- | --- |
| `text` | `string` | До 200 символов. |

Результат: `true`

### `storage.snapshot` {#storage.snapshot}

Всё состояние общих данных на одном `seq` — см. «Общие данные».

Результат: `{ seq: number, private: object, stores: { [name]: Store } }`

### `storage.write` {#storage.write}

Одна запись в общее хранилище; операции — в справочнике хранилища. Изменение приходит всем участникам, и автору тоже, событием `storage.changed` на своём месте в порядке; обычно автору — до ответа на вызов, но полагаться на это нельзя.

| Параметр | Тип | Что |
| --- | --- | --- |
| `op` | `string` | Операция: `log.append`, `log.retract`, `slot.set`, `slot.clear`, `doc.set`, `counter.add`. |
| `store` | `string` | Имя хранилища из манифеста. |
| `…` |  | Поля операции. |

Результат: `{ seq: number, … }`

```js
await pairx.call('storage.write', { op: 'log.append', store: 'queue', value: 'join' });
```

### `storage.private.set` {#storage.private.set}

Личные данные человека. Обычно не вызывается напрямую: это делает `localStorage`.

| Параметр | Тип | Что |
| --- | --- | --- |
| `items` | `{ [key]: string }` | Все пары ключ — строка, до 256 КиБ вместе. |

Результат: `{ ok: true }`

### `clipboard.writeText` {#clipboard.writeText}

Скопировать текст, до 10 000 символов; только по нажатию человека.

| Параметр | Тип | Что |
| --- | --- | --- |
| `text` | `string` | Текст. |

Результат: `true`

### `diagnostics.error` {#diagnostics.error}

Сообщить об ошибке приложения. Сейчас Pairx принимает вызов и никуда его не отправляет; отчёты для автора придут позже, без данных хранилища.

| Параметр | Тип | Что |
| --- | --- | --- |
| `message` | `string` | Что случилось. |

Результат: `true`

## События {#events}

| Событие | Данные | Когда |
| --- | --- | --- |
| `storage.changed` | `{ changes: Change[] }` | Изменения общих данных от всех участников, по порядку `seq` — см. «Формы изменений». |
| `storage.reset` | `null` | Перечитайте снимок: кто-то удалил свои данные или изменений слишком много. |
| `theme.changed` | `Theme` | Тема Pairx сменилась при открытом приложении (сейчас на iPhone; на Android тема задаётся при открытии). |
| `ui.mainButton.clicked` | `null` | Нажата нативная кнопка Pairx. |
| `ui.backButton.clicked` | `null` | Нажата «Назад» в шапке или системный жест «назад». |

## Ошибки {#errors}

Ошибка вызова — `Error` с полем `code`:

| Код | Когда |
| --- | --- |
| `unsupported` | Метода нет, его параметры не те или Pairx в этой версии его не знает. |
| `forbidden` | Нельзя: не хватает прав (например, писать в хранилище автора) или чужая запись. |
| `consent_required` | Человек ещё не дал приложению разрешение. |
| `rate_limited` | Слишком часто: больше 10 записей в секунду или 300 в минуту от человека. |
| `too_large` | Слишком большое значение, запись, вызов (больше 64 КиБ) или превышен объём хранилища. |
| `conflict` | Документ уже изменён: версия устарела — перечитайте снимок и повторите. |
| `offline` | Нет соединения с Pairx; запись можно повторить с тем же write_id. |
| `not_found` | Нет такой записи, хранилища или контекста (например, сообщение с приложением удалили). |
| `internal` | Ошибка на стороне Pairx. |

## Параметры запуска {#launch}

`pairx.launch` есть сразу, без вызова:

| Поле | Тип | Что |
| --- | --- | --- |
| `app` | `string` | ID приложения, 22 символа. |
| `context` | `"chat" \| "personal" \| "preview"` | Где открыто: общий экземпляр в чате, личный контекст (по ссылке, из профиля) или предпросмотр автора. |
| `version` | `number` | Номер версии приложения. |
| `token` | `string?` | Подписанный токен запуска (Ed25519, aud — ID приложения); пригодится серверам мини-приложений. |
| `user` | `string?` | ID человека внутри этого приложения. |

## Токен запуска {#launch-token}

`pairx.launch.token` — JWT, который Pairx подписывает при каждом открытии ключом Ed25519. Он нужен, когда у приложения появится свой сервер: сервер проверяет подпись по открытым ключам Pairx и узнаёт, кто и где открыл приложение, без общих секретов. Само приложение токену доверять не должно — внутри песочницы его можно только передать.

Алгоритм `EdDSA`, издатель `pairx-apps`, срок — 60 минут. Открытые ключи — `https://api.pairx.org/.well-known/pairx-apps-keys.json` (JWKS; ключ выбирается по `kid` из заголовка токена).

| Поле | Что |
| --- | --- |
| `iss` | Всегда `pairx-apps`. |
| `aud` | ID приложения: токен одного приложения бесполезен для другого. |
| `sub` | ID человека внутри приложения — тот же, что в `user.get`. |
| `iat, exp` | Время выдачи и окончания, секунды Unix; токен живёт час. |
| `jti` | Уникальный ID токена. |
| `ver` | Номер запущенной версии. |
| `ctx` | Контекст: `kind` (`chat`, `personal`, `preview`), `id`, `can_write`, `author`; в чате ещё `chat` (`dm`, `group`, `self`) и `role` (`owner`, `admin`, `member`). |
| `start` | Параметр из ссылки `pairx.org/app/<id>?start=…`, если он был. |
| `name` | Имя человека — только с разрешением `profile.basic`. |

## Будет позже {#later}

Методы, которые появятся в следующих версиях моста. Проверяйте их через `app.supports`, а не по версии Pairx:

- `ui.secondaryButton` — Вторая нативная кнопка.
- `share.toChat` — Отправить приложение с параметром в другой чат.
- `links.openApp` — Открыть другое приложение Pairx.
- `context.members` — Участники чата (с разрешением `chat.members`).
- `user.get → name` — Имя человека (с разрешением `profile.basic`).
