@tinrun/client
v1.0.0
Published
Library for the page of an app opened in tin (tin.bar/<id>): user, launch data, theme, navigation. SDK v1 of the tin app standard.
Readme
@tinrun/client
Библиотека для страницы приложения tin — сайта, который tin открывает во всю страницу по адресу
tin.bar/<id>. Через неё страница узнаёт пользователя и получает подписанные данные запуска для своего сервера,
следует теме, ведёт адрес и историю в tin. Это SDK v1 стандарта приложения tin (specs/app-standard.md).
Для сервера приложения — отдельный пакет @tinrun/server (проверка пользователя, токен tinAPI, MCP).
Быстрый старт
// npm i @tinrun/client
import { connect } from '@tinrun/client'
const tin = await connect({ spa: true }) // SPA (Vue Router, React Router …) — spa: true
tin.user // { id, walletAddress } — кто открыл (null — только гость, см. «Гостевой доступ»)
tin.launchData // подписанные данные запуска: отправьте своему серверу, он проверит пользователя
tin.ready() // первый экран показан — tin убирает заставкуБез сборщика — скрипт с адреса tin (глобальный TinClient):
<script src="https://tin.bar/sdk/v1/tin-client.js"></script>
<script>
TinClient.connect({ spa: true }).then((tin) => {
/* … */
tin.ready()
})
</script>/sdk/tin-sdk.js и глобальное имя TinSDK — тот же файл для уже подключивших (старое имя библиотеки).
Весь интерфейс
| | Имя | Что |
|---|---|---|
| Контекст | tin.version | версия протокола хоста (tin.hostVersion — то же) |
| | tin.theme | 'light' \| 'dark' |
| | tin.user | { id, walletAddress } (walletAddress — raw TON, 0:…); null — только гость |
| | tin.launchData | подписанные данные запуска (JWT) для сервера приложения; null у гостя |
| | tin.startParam | параметр из ссылки tin.bar/<id>?start=<значение> ([A-Za-z0-9_-]{1,64}) или null; тот же — в launchData |
| Методы | ready() | приложение готово, убрать заставку |
| | getLaunchData() | свежие launchData (не чаще раза в 10 с) |
| | setTitle(title) | заголовок вкладки (до 100 символов; '' — название приложения) |
| | openLink(url) | открыть http(s) ссылку в новой вкладке |
| | close() | выйти из приложения |
| | requestSignIn() | вход (только гость, «Гостевой доступ»; вошедший получает пользователя сразу) |
| События | themeChanged | { theme } |
| | userChanged | { user, launchData } — только гостевой доступ |
| | visibilityChanged | { visible } — пользователь ушёл в другое приложение (iframe скрыт, но живёт) или вернулся |
tin.on(event, handler) возвращает функцию отписки. tin.call(method, params) — вызов метода по имени.
Навигацию (setLocation, go, navigate) библиотека делает сама; вручную — только с syncLocation: false.
Живые примеры всего этого — demo.html (/demo в npm run dev tinWeb).
connect(options)
| Опция | По умолчанию | Что |
|---|---|---|
| spa | false | приложение — SPA: «Назад»/свайпы двигают роутер без перезагрузки даже после обновления страницы |
| hostOrigin | origin скрипта | какому tin доверять ('https://tin.bar'). Указывайте при установке из npm: иначе доверие строится по встраивающей странице |
| timeout | 10000 | сколько ждать ответа tin, мс |
| syncLocation | true | адрес и история приложения ведутся в tin (см. ниже) |
Ошибки — TinSdkError с полем code: not_connected (не внутри tin), unknown_method (tin старее библиотеки),
invalid_params, cancelled (пользователь отказался), not_allowed (сейчас нельзя: requestSignIn без клика,
getLaunchData чаще раза в 10 с), failed.
Открытие
- Приложение открывает только вошедший пользователь: гость по ссылке видит экран входа tin, приложение не загружается (исключение — гостевой доступ).
- tin загружает iframe и показывает свою заставку до
tin.ready(). Нет сигнала за 15 с — «Приложение не отвечает» с кнопкой «Повторить». - Приложение сразу получает
tin.userиtin.launchData. - Выход из tin выгружает приложение, вход другим пользователем загружает его заново — обрабатывать смену пользователя не нужно.
- Уход в другое приложение не выгружает ваше: iframe скрывается и приходит
visibilityChanged: { visible: false }— закройте долгие потоки (SSE, WebSocket), остановите таймеры. Возврат —{ visible: true }, состояние на месте. tin держит несколько последних приложений; самое давнее выгружается. Переход по ссылке с другим?start=в уже открытое приложение открывает его заново — со свежимиlaunchDataи новымtin.startParam.
Гостевой доступ (только наши приложения)
Приложение открывается гостю с user: null и launchData: null. Вход — tin.requestSignIn() из обработчика
клика: tin скрывает приложение и показывает свой экран входа (кошелёк подключает только tin), после входа —
промис с пользователем и событие userChanged с user и launchData, без перезагрузки. Отказ — cancelled.
Данные запуска и сервер приложения
launchData — JWT, подписанный tin ключом Ed25519: typ: tin-launch+jwt, kid, iss: tin, aud = id
приложения, sub = id пользователя, wallet, start_param, срок 1 час. Фронтенд отправляет его своему
серверу; сервер обязан проверить (фронтенду не доверять):
- подпись — по
kidоткрытым ключом изhttps://tin.bar/api/apps/launch-keys(JWKS, кэшировать до часа); typ = tin-launch+jwt,alg = EdDSA,iss = tin;aud= id своего приложения — без этого пройдут данные, выданные другому приложению;expне истёк. Незнакомые поля игнорировать.
Проще всего — библиотека @tinrun/server (verifyLaunchData). Без неё, на Node с jose:
import { createRemoteJWKSet, jwtVerify } from 'jose'
const launchKeys = createRemoteJWKSet(new URL('https://tin.bar/api/apps/launch-keys'))
export async function verifyLaunchData(launchData: string) {
const { payload } = await jwtVerify(launchData, launchKeys, {
algorithms: ['EdDSA'], typ: 'tin-launch+jwt', issuer: 'tin', audience: process.env.TIN_APP_ID,
})
return { id: payload.sub!, walletAddress: payload.wallet as string, startParam: payload.start_param ?? null }
}Нужен tinAPI от имени пользователя — сервер меняет launchData на access-токен (15 минут, права приложения):
POST https://tin.bar/api/apps/token
Authorization: Bearer tin_app_… ← токен приложения, только на сервере
{ "launchData": "<JWT>" }
→ { "accessToken", "accessTokenExpiresAt", "user": { "id", "walletAddress" } }Refresh-токенов нет: истёк токен или пришёл 401 — страница берёт tin.getLaunchData(), сервер меняет снова.
Обязательные правила для страницы
- Сессия — не в cookies: приложение внутри tin — сторонний iframe (Safari блокирует его cookies). Сессия — в
памяти и в заголовке
Authorization; после перезагрузки — снова изlaunchData. - Никакого своего входа и подключения кошелька: пользователь — только из
launchData. Никогда не просить сид-фразу, пароли, ключи. - HTTPS;
Content-Security-Policy: frame-ancestors https://tin.bar. tin.ready()— как только показан первый экран (≤ 15 с).- Тема — из
tin.themeиthemeChanged; внешние ссылки —tin.openLink; выход —tin.close(). - Вёрстка работает от 360 px ширины.
- SPA —
connect({ spa: true }); формы и переходы вне роутера — черезlocation.replace().
Адрес и история
Каждый экран приложения — адрес tin.bar/<id><хвост> ⇄ <url приложения><хвост> и шаг истории tin. Например,
для url: 'https://shop.example/app/': tin.bar/shop/cart?id=1 ⇄ https://shop.example/app/cart?id=1.
- Переходы (
pushStateроутеров, ссылки, якоря) библиотека сообщает tin, и tin добавляет или заменяет запись своей истории. Сам iframe записей не создаёт:pushState→replaceState, ссылки — черезlocation.replace(). - «Назад», «Вперёд», свайпы листают записи tin; библиотека переводит роутер через
popstateбез перезагрузки. С первого экрана «Назад» выходит из приложения.close()выходит сразу. history.back()/router.back()отдаются tin: дальше страницы, с которой открыли приложение, не уводят.- Ссылка или обновление страницы открывают iframe сразу на адресе из хвоста.
- Не перехватываются формы и
location.assign(): там используйтеlocation.replace(). - Хвост не может увести iframe на другой origin, адрес вне
urlприложения tin отклоняет (invalid_params).
Совместимость
v1 (/sdk/v1/tin-client.js, npm @tinrun/[email protected]) меняется только добавлениями: новые методы, события,
необязательные поля. Ломающее — v2 по новому адресу; v1 работает ещё минимум 12 месяцев после выхода v2.
Для разработчиков tinWeb
Код библиотеки — src/sdk/ репозитория tinWeb (импортирует только свою папку). Протокол — protocol.ts,
общий для библиотеки и tinWeb:
- Страница шлёт родителю
connectчерезpostMessage(повторяет, пока хост не ответит;page— случайный id страницы, хост отвечает каждой странице один раз). - tinWeb проверяет, что сообщение пришло из его iframe и с origin приложения из реестра tinAPI, дожидается
launchData(их запрос идёт параллельно с загрузкой iframe) и отвечаетconnectedс контекстом иMessagePort— только на origin приложения. - Дальше всё идёт через порт: запросы
request→responseи событияevent.
Где что в tinWeb: src/modules/apps/ — AppsHost.vue (живые iframe, до VITE_APPS_LIVE_MAX = 3), AppView.vue
(порядок открытия, история, смена пользователя), AppFrame.vue (iframe, обработчики методов, заставка до ready()),
AppGate.vue (экраны входа, «не найдено», «заблокировано», «не отвечает»), apps.ts (реестр GET /api/apps/:id,
launch, предзагрузка VITE_PRELOAD_APPS).
Добавить метод или событие: описать в HostMethods / HostEvents (protocol.ts, только совместимо; ломающее —
VERSION + 1), обёртку — в TinApp (index.ts), обработчик — в AppFrame.vue (параметры из чужого кода проверять),
пример — в demo.html, строку — в таблицу выше и в specs/app-standard.md.
Сборка и публикация: npm run build:sdk → dist-sdk/ (пакет: index.js — ES-модуль, tin-client.js — скрипт,
*.d.ts, package.json из src/sdk/package.json, этот README). tinWeb сам отдаёт скрипт по /sdk/v1/tin-client.js
и /sdk/tin-sdk.js (плагин tinSdk в vite.config.ts). Публикация в npm — из CI по тегу client-v<версия>
(.github/workflows/publish-client.yml, trusted publishing — README tinWeb, «Публикация»); версия в теге должна совпадать с
src/sdk/package.json, 1.x — SDK v1.
Добавить приложение: регистрация — в tinAPI (npm run apps:register, README tinAPI). Приложение должно жить на
другом origin, чем tinWeb (iframe с sandbox="allow-scripts allow-same-origin …"). Демо demo.html (/demo,
только в npm run dev) — исключение для разработки; launchData оно получает, если demo зарегистрировано в
локальном tinAPI.
