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

@alexandermo/talent-oauth-pkce

v0.1.1

Published

Универсальный браузерный клиент OAuth2 Authorization Code + PKCE без зависимостей

Downloads

368

Readme

OAuth PKCE для SPA

Библиотека для входа через Талант из браузерного приложения. Открывает страницу авторизации, обрабатывает возврат после входа и поддерживает сессию. Работает с React, Vue и обычным JavaScript, без зависимостей.

Сервер хранит токены в HttpOnly-cookie. Библиотека работает в одном из двух режимов:

  • mode: 'token' (по умолчанию) — библиотека берёт access_token из ответа сервера и держит его в памяти. Приложение читает токен через auth.getAccessToken() и передаёт в свои API-запросы в заголовке Authorization: Bearer <token>. Токен не сохраняется в localStorage или sessionStorage. После перезагрузки страницы ensureSession() получает новый токен через refresh по cookie.
  • mode: 'cookie' — прежнее поведение: авторизация только через cookie, библиотека токен не читает, getAccessToken() всегда возвращает undefined.

Собственные запросы библиотеки (проверка, обновление, выход) в обоих режимах идут только с cookie.

Подключение

npm install @alexandermo/talent-oauth-pkce

Создайте один экземпляр на всё приложение, вне компонентов:

// auth.ts
import { createAuth } from '@alexandermo/talent-oauth-pkce';

export const auth = createAuth({
  clientId: 'your-client-id',
  redirectUri: `${window.location.origin}/auth/complete`,
  onError(error) {
    // Здесь можно показать сообщение о сбое фонового обновления.
    console.warn(error.message);
  },
});

По умолчанию используется production. Зарегистрируйте redirectUri в настройках OAuth-приложения и добавьте в роутер страницу /auth/complete. Адрес должен быть абсолютным, без #, с теми же протоколом, доменом и портом, что и приложение.

Для работы нужны HTTPS или localhost и доступный sessionStorage: в нём на 10 минут сохраняются данные попытки входа.

Вход, проверка сессии и выход

Ниже — отдельные обработчики приложения. router, showError, loadProfile и clearUser — ваш роутер и функции для работы с интерфейсом и профилем.

import { auth } from './auth';

// Кнопка «Войти».
async function signIn() {
  try {
    await auth.login({ next: '/games' });
  } catch {
    showError('Не удалось начать вход. Попробуйте ещё раз.');
  }
}

// Страница /auth/complete.
async function completeSignIn() {
  try {
    const { next } = await auth.completeLogin();
    await router.replace(next);
  } catch {
    showError('Не удалось завершить вход. Начните вход заново.');
  }
}

// Перед показом защищённой части приложения.
async function startApp() {
  try {
    await auth.ensureSession();
    await loadProfile();
  } catch {
    showError('Не удалось загрузить данные. Повторите попытку или войдите заново.');
  }
}

// Кнопка «Выйти».
async function signOut() {
  try {
    await auth.logout();
  } catch {
    showError('Не удалось завершить выход. Попробуйте ещё раз.');
    return;
  }
  clearUser();
  await router.replace('/login');
}

next — путь внутри приложения, например /games?tab=drafts; по умолчанию /. Внешние адреса не допускаются. completeLogin() вызывайте только на странице callback. Страницы входа и callback должны открываться без ensureSession().

После входа или успешной проверки библиотека сама обновляет сессию. Профиль, права и переходы между страницами остаются в приложении.

Примеры для React, Vue 2, Vue 3, Axios и fetch

API и настройки

| Метод | Что делает | | ------------------ | ---------------------------------------------------------------------------------- | | login({ next? }) | Сохраняет попытку входа и переводит браузер на страницу авторизации. | | completeLogin() | Обменивает код на сессию и возвращает { next: string }. | | ensureSession() | Проверяет сессию и при необходимости обновляет её. | | refresh() | Принудительно обновляет сессию. | | getAccessToken() | Текущий access_token или undefined (нет токена, истёк, режим 'cookie'). | | logout() | Завершает серверную сессию. После этого можно снова вызвать login(). | | destroy() | Останавливает экземпляр, отменяет запросы и убирает таймеры и обработчики событий. |

Все методы, кроме getAccessToken() и destroy(), возвращают Promise. completeLogin() возвращает данные перехода, остальные — Promise<void>. destroy() возвращает void и не завершает серверную сессию. После него нужен новый экземпляр; не вызывайте его при размонтировании отдельного компонента.

В createAuth() обязательны только clientId и redirectUri. Остальные настройки:

| Параметр | Для чего нужен | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | environment | 'production' по умолчанию или 'staging'. | | apiBaseUrl | Общая база API, только для environment === staging и локальной разработки. нужно чтобы корректно настроить прокси при разработке. Пример ниже. | | authorizeUrl | Свой абсолютный адрес страницы входа. | | tokenUrl, introspectUrl, logoutUrl | Отдельные адреса API, абсолютные или относительные. | | scope | Запрашиваемые права, например 'profile'. | | refreshHeaders | Дополнительные заголовки только для refresh, например { 'X-Ignore-Access-Token': '1' }. | | refreshBeforeTokenExpirySeconds | За сколько секунд до истечения обновлять сессию. По умолчанию 15; допустимо конечное число от 0. | | onError | Обработчик ошибок фоновой проверки и обновления. | | mode | 'token' по умолчанию (токен доступен через getAccessToken()) или 'cookie' (только cookie, как в прежних версиях). | | onRemoteLogout | Вызывается, когда выход выполнен в другой вкладке этого приложения; локальная сессия к этому моменту уже сброшена. | | onAccessTokenChange | Вызывается при каждой смене токена; undefined — токен сброшен. Для UI и стора, не для подстановки в запросы. |

Production использует https://talent.kruzhok.org, staging — https://talent.test.kruzhok.org. Страница входа находится на /authorize, база API — /v2. К базе добавляются пути oauth/issue-token, oauth/introspect и users/me/logout.

Приоритет адресов: отдельный URL → apiBaseUrl → адрес среды. authorizeUrl от базы API не зависит. Настройки задаются при создании экземпляра; только undefined включает значение по умолчанию. Некорректные настройки вызывают AuthError с кодом config_invalid.

apiBaseUrl и локальная разработка

apiBaseUrl можно передать только вместе с environment: 'staging'. Подойдёт полный адрес с префиксом API (https://talent.test.kruzhok.org/v2) или путь от корня сайта (/admin/talent_v2_api). Не добавляйте логин, пароль, query или hash; пустые строки, пробелы, обратные слеши и null не допускаются. Завершающий / необязателен.

Если база уже есть в переменной baseURL, передайте apiBaseUrl: baseURL. Это должен быть именно префикс API Таланта, к которому можно добавить oauth/issue-token, а не адрес другого API приложения.

Для Vite можно заменить auth.ts из первого примера следующим вариантом:

import { createAuth } from '@alexandermo/talent-oauth-pkce';
import type { AuthConfig } from '@alexandermo/talent-oauth-pkce';

const common = {
  clientId: 'your-client-id',
  redirectUri: `${window.location.origin}/auth/complete`,
};

const config: AuthConfig = import.meta.env.DEV
  ? {
      ...common,
      environment: 'staging',
      apiBaseUrl: '/admin/talent_v2_api',
    }
  : {
      ...common,
      environment: import.meta.env.VITE_OAUTH_ENVIRONMENT === 'staging' ? 'staging' : 'production',
    };

export const auth = createAuth(config);

Две ветки сохраняют связь между средой и базой API: в production поле apiBaseUrl отсутствует. Если задать среду и базу двумя независимыми условиями, TypeScript может увидеть запрещённое сочетание production с apiBaseUrl.

Локально этот пример всегда выбирает staging и прокси. Для сборки стенда задайте VITE_OAUTH_ENVIRONMENT=staging; любое другое значение, включая отсутствие переменной, выбирает production. В сборке используются прямые адреса выбранной среды. Сама библиотека переменные Vite не читает.

Добавьте прокси в vite.config.ts:

import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      '/admin/talent_v2_api': {
        target: 'https://talent.test.kruzhok.org',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/admin\/talent_v2_api/, '/v2'),
        cookieDomainRewrite: '',
        cookiePathRewrite: '/',
      },
    },
  },
});

Так запрос /admin/talent_v2_api/oauth/issue-token попадёт на https://talent.test.kruzhok.org/v2/oauth/issue-token. Прокси убирает Domain у cookie и заменяет явно заданный Path на /; выдача и удаление cookie должны использовать одинаковые Domain/Path. HttpOnly, Secure и SameSite сохраняются. Если cookie не работают по локальному HTTP, используйте HTTPS и проверьте настройки cookie на сервере.

Токен доступа

В режиме 'token' библиотека — единственное место, где хранится токен. Чтобы токен в приложении не расходился с токеном библиотеки:

  • читайте auth.getAccessToken() перед каждым запросом и не сохраняйте копию в сторе, localStorage или замыкании;
  • на 401 от своего API вызывайте await auth.refresh() и повторяйте запрос с новым getAccessToken(). Параллельные вызовы refresh() объединяются в один запрос, поэтому все повторы получат один и тот же новый токен;
  • onAccessTokenChange используйте только для реакции интерфейса, например чтобы показать, что пользователь вошёл.

Токен обновляется вместе со сроком сессии: после completeLogin(), refresh() и фонового обновления. Он сбрасывается сразу при вызове logout(), при destroy() и при окончательном отказе обновления. При временном сбое (сеть, 429, 5xx) токен остаётся до истечения срока. getAccessToken() не возвращает истёкший токен. Ответ сервера, пришедший после начала выхода, токен не восстанавливает.

В режиме 'token' ответ issue-token обязан содержать access_token; без него вход и обновление завершаются ошибкой invalid_response. Если сервер его не возвращает, укажите mode: 'cookie'.

Пример — Axios и fetch с заголовком Authorization.

Переход с прежней версии

Раньше библиотека работала только с cookie. Теперь по умолчанию включён режим 'token'. Чтобы сохранить прежнее поведение, добавьте mode: 'cookie' в createAuth().

Ошибки и поведение сессии

Ошибки явных вызовов обрабатывайте через try/catch. onError получает только ошибки фоновых операций. У AuthError есть code, message, необязательный HTTP status и retriable:

import { AuthError } from '@alexandermo/talent-oauth-pkce';
import { auth } from './auth';

try {
  await auth.ensureSession();
} catch (error) {
  if (error instanceof AuthError && error.retriable) {
    showError('Сервер временно недоступен. Повторите попытку позже.');
  } else {
    showError('Не удалось проверить сессию. Попробуйте войти заново.');
  }
}

retriable равен true для сетевых ошибок, 429 и 5xx. Такой сбой сам по себе не означает, что пользователь вышел. После временного сбоя фонового обновления библиотека повторяет попытку через 30 секунд; окончательный отказ останавливает обновление.

logout() отправляет POST с cookie и ожидает 204 No Content. Пока выход выполняется, остановите загрузку защищённых данных: новые операции авторизации отклоняются с session_interrupted. Не запускайте новый вход автоматически в ответ на эту ошибку.

Если logout() завершился с ошибкой, покажите пользователю кнопку «Повторить выход», которая снова вызовет logout(). Библиотека сама не повторяет выход и больше не обновляет сессию в фоне. При обрыве связи результат выхода неизвестен: сервер мог выполнить запрос, но ответ не дошёл до браузера.

Если пользователь хочет продолжить работу, вызовите ensureSession(). При успешной проверке автоматическое обновление возобновится. Если сессию восстановить не удалось, предложите повторить проверку или войти заново.

Все OAuth-запросы отправляются с credentials: 'include'. Для прямых запросов на другой домен сервер должен разрешать CORS с cookie, а браузер — отправку этих cookie. В запросах приложения также включайте cookie (credentials: 'include' для fetch или withCredentials: true для Axios), а в режиме 'token' — заголовок Authorization.

Библиотека объединяет параллельные проверки и обновления внутри одного экземпляра. Между вкладками синхронизируется только выход — см. Несколько вкладок. При возврате на вкладку и восстановлении сети ранее установленная сессия перепроверяется, но обработка 401 от вашего API всё равно нужна — примеры.

Несколько вкладок

У каждой вкладки свой экземпляр и свой токен в памяти. Сервер не отзывает access_token при выходе, поэтому без синхронизации другие вкладки могли бы работать со старым токеном до конца его срока.

После успешного logout() (ответ 204) библиотека сообщает о выходе остальным вкладкам того же приложения через BroadcastChannel. Сообщение не содержит токенов, а приложения различаются по clientId и redirectUri. Получив его, вкладка сразу:

  • сбрасывает токен, срок сессии и таймер обновления;
  • отклоняет свои незавершённые операции с session_interrupted, чтобы их ответы не восстановили токен;
  • вызывает onRemoteLogout.

В onRemoteLogout очистите данные пользователя и перейдите на страницу входа — пример. Если выход завершился ошибкой, другие вкладки не затрагиваются. В браузерах без BroadcastChannel синхронизации нет, остальное работает как обычно.

Это мера для интерфейса, а не защита: уже скопированный токен продолжит действовать до истечения срока. Если сообщение не дошло (вкладка была выгружена браузером), вкладка узнает о выходе при следующем обновлении: оно завершится ошибкой в onError или в catch вашего обработчика 401. Вход в другой вкладке синхронизировать не нужно: ensureSession() получит токен по cookie.

Разработка

Для разработки используйте Node.js 24.16 или новее.

npm ci
npm run typecheck
npm run lint
npm run format:check
npm test
npm run build
npm run smoke

npm test запускает тесты Vitest из tests/ без предварительной сборки. Для повторного запуска при изменениях используйте npm run test:watch. Тесты подменяют браузерные API и время.

dist/ создаётся при сборке. npm run smoke проверяет импорт собранного пакета в форматах ESM и CommonJS без браузера. Настоящие редиректы, cookie/CORS, выход и работу нескольких вкладок проверьте в приложении с вашим сервером.