npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

Открытие

  1. Приложение открывает только вошедший пользователь: гость по ссылке видит экран входа tin, приложение не загружается (исключение — гостевой доступ).
  2. tin загружает iframe и показывает свою заставку до tin.ready(). Нет сигнала за 15 с — «Приложение не отвечает» с кнопкой «Повторить».
  3. Приложение сразу получает tin.user и tin.launchData.
  4. Выход из tin выгружает приложение, вход другим пользователем загружает его заново — обрабатывать смену пользователя не нужно.
  5. Уход в другое приложение не выгружает ваше: 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 час. Фронтенд отправляет его своему серверу; сервер обязан проверить (фронтенду не доверять):

  1. подпись — по kid открытым ключом из https://tin.bar/api/apps/launch-keys (JWKS, кэшировать до часа);
  2. typ = tin-launch+jwt, alg = EdDSA, iss = tin;
  3. aud = id своего приложения — без этого пройдут данные, выданные другому приложению;
  4. 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(), сервер меняет снова.

Обязательные правила для страницы

  1. Сессия — не в cookies: приложение внутри tin — сторонний iframe (Safari блокирует его cookies). Сессия — в памяти и в заголовке Authorization; после перезагрузки — снова из launchData.
  2. Никакого своего входа и подключения кошелька: пользователь — только из launchData. Никогда не просить сид-фразу, пароли, ключи.
  3. HTTPS; Content-Security-Policy: frame-ancestors https://tin.bar.
  4. tin.ready() — как только показан первый экран (≤ 15 с).
  5. Тема — из tin.theme и themeChanged; внешние ссылки — tin.openLink; выход — tin.close().
  6. Вёрстка работает от 360 px ширины.
  7. 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:

  1. Страница шлёт родителю connect через postMessage (повторяет, пока хост не ответит; page — случайный id страницы, хост отвечает каждой странице один раз).
  2. tinWeb проверяет, что сообщение пришло из его iframe и с origin приложения из реестра tinAPI, дожидается launchData (их запрос идёт параллельно с загрузкой iframe) и отвечает connected с контекстом и MessagePort — только на origin приложения.
  3. Дальше всё идёт через порт: запросы 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.