Telegram authentication plugin for Better Auth. Login Widget + Mini App + OIDC auth flows, published as better-auth-telegram on npm.
Two entry points, two export paths:
src/index.ts->better-auth-telegram-- Server plugin (BetterAuthPlugin)src/client.ts->better-auth-telegram/client-- Browser client plugin (widget management, Mini App auto-signin)
Supporting modules:
src/client-widget.tsandsrc/client-types.ts-- Browser widget lifecycle and client response/widget types.client.tskeeps the API actions, Mini App helpers, session notifications and public exports.src/verify.ts-- HMAC-SHA256 verification via Web Crypto API. Login Widget usesSHA256(botToken); Mini App usesHMAC-SHA256("WebAppData", botToken).src/oidc.tsandsrc/oidc-token.ts-- Better Auth 1.7 provider factory (accountSubject=sub, PKCE, nonce, Basic client authentication) and cached JWKS/token verification, including profile-ID normalization before claims are used.src/oidc-linking.tsandsrc/oidc-linking-policy.ts-- Match a verified OIDC sign-in to an existing Telegram user before Better Auth's account lookup. Require linked identity proof, preserve existingsubownership, and enforce implicit-linking policy and validation hooks.src/telegram-account.ts-- Find-or-create orchestration for Login Widget and Mini App users.telegram-account-state.tsowns shared types, lookups, validation and per-instance operation locks;telegram-account-user.tsowns provisioning and profile refresh;telegram-account-link.tsowns explicit link/unlink. Writes use Better Auth's internal adapter.src/telegram-session.ts-- Creates sessions and renews cookies for Login Widget and Mini App flows.src/telegram-identity.ts-- Installs database hooks.telegram-identity-state.tsholds verified request claims;telegram-identity-sync.tscopies and reconciles linked identities;telegram-identity-session.tsrefreshes OIDC session cookies;telegram-profile.tsapplies mapped profile updates;telegram-placeholder.tsreserves placeholder emails and chooses collision-safe OIDC placeholders.src/testing/harness.ts-- Stable test-fixture imports. Implementations live intesting/auth.ts(memory/SQLite Better Auth),http-client.ts(cookie-aware requests),signed-data.ts(Widget/Mini App fixtures),oidc-server.ts(fake Telegram),oidc-flow.ts(sign-in helpers) andconstants.ts(independent fixture constants).src/testing/adapters.ts-- Real PostgreSQL/MySQL (Kysely), Drizzle, Prisma and MongoDB fixtures for*.adapter.test.ts; each lane requires itsBAT_TEST_*_URL.src/types.tsandsrc/constants.ts-- Public server types, defaults, provider IDs and OIDC endpoints. Client types live inclient-types.ts; widget options live inclient-widget.ts.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /telegram/signin |
None | Authenticate with Login Widget data |
| POST | /telegram/link |
Session | Link Telegram to current user |
| POST | /telegram/unlink |
Session | Unlink Telegram from current user |
| GET | /telegram/config |
None | Returns bot username for widget init |
| POST | /telegram/miniapp/signin |
None | Sign in from Mini App (optional) |
| POST | /telegram/miniapp/validate |
None | Validate Mini App initData (optional) |
OIDC (when oidc.enabled) uses Better Auth's built-in social login routes (POST /sign-in/social with provider: "telegram-oidc", GET /callback/telegram-oidc). The plugin injects a telegram-oidc social provider via the init hook.
Schema conditionally extends user table with telegramId, telegramUsername, and telegramPhoneNumber fields, and account table with telegramId and telegramUsername fields — only when Login Widget or Mini App flows are enabled (default). OIDC-only setups (loginWidget: false) skip these fields.
- Biome via ultracite (
biome.jsoncextendsultracite/biome/coreandultracite/biome/vitest;examples/andtest/are excluded) - lint-staged runs
ultracite fixon pre-commit - Relaxed rules:
noNonNullAssertion,noExplicitAny,useNamingConvention,noMagicNumbersall off - ES modules,
verbatimModuleSyntax, strict null checks,noUncheckedIndexedAccess
For test authoring, review, pruning or verification decisions, use /testing-patterns (shared instructions).
- Vitest with
happy-domenvironment;*.integration.test.tsrun innodeagainst a real Better Auth instance (seesrc/testing/harness.ts). Prefer those over mocking Better Auth: mock-only tests passed while OIDC was broken on 1.7. - Tests co-located in
src/(*.test.ts). OIDC integration suites separate sign-in, protocol, identity, profile, claims and linking; Widget, Mini App and mapper suites cover their corresponding flows. npm run test:typescompilestest-d/consumer.tsagainst the builtdist/(runnpm run buildfirst)*.adapter.test.ts(the adapter lane) is excluded fromnpm test; CI runs it in theadaptersjob with service containers. Concurrency and driver typing (PostgreSQL integer ids) only show up there: SQLite serializes every query.- Better Auth disables its origin checks under
NODE_ENV=test; tests of CSRF behavior setadvanced.disableOriginCheck: false. - Coverage: v8 provider, thresholds -- 90% lines/statements/branches, 80% functions
vitest.setup.tssuppresses happy-dom DOMException warnings for script loading
npm run build # tsdown (ESM + CJS + .d.ts, publint + attw)
npm run dev # tsdown --watch
npm run test:types # type-check a consumer of dist/
npm run test:package # build first; install and verify the release tarball
npm run type-check # tsc --noEmit
npm run test # vitest run
npm run test:watch # vitest (watch mode)
npm run test:coverage # vitest run --coverage
npm run test:adapters # real databases; needs BAT_TEST_POSTGRES_URL / _MYSQL_URL / _MONGODB_URL (see vitest.adapters.config.ts)
npm run lint # ultracite check
npm run lint:fix # ultracite fixSingle test file: npx vitest run src/verify.test.ts
Runtime (peer): better-auth, @better-auth/core (>=1.7.17 <1.8.0; 1.7.7 fixed GHSA-965c-763c-88jm, 1.7.12 enforced ID-token nonces across callbacks; 1.7.17 includes live-session checks for OAuth linking)
Runtime (dependency): jose
Build: tsdown keeps peers and dependencies external; better-call types are inlined into the .d.ts on purpose (not our dependency)
Dev: Biome 2.6 via ultracite 8, tsdown, TypeScript 7, Vitest 5, happy-dom
Node: >= 24.0.0 (engines.node, tested on 24.0.0, current 24 and 26 in CI); 24 for development (.nvmrc)
- Verification logic in
verify.tsis security-critical -- changes to HMAC computation or timestamp checks need extra scrutiny botTokenmust never leak to client-side code. Server plugin only.- All endpoints use
createAuthEndpoint()frombetter-auth/api. Follow that pattern. - Write users/accounts through
ctx.context.internalAdapter, never rawctx.context.adapter.create(that skipsdatabaseHooksandvalidateUserInfo). Reads viaadapter.findOneare fine. - Telegram facts verified live (2026-10-29): OIDC
subis per bot (use theidclaim for the Telegram user id; the plugin normalizes verified numeric or string IDs to strings before exposing claims); switching a bot to OIDC permanently disables its legacy Login Widget; BotFather rejects localhost redirect URIs; ID tokens live 30 s; token errors come back as HTTP 200; declining the OIDC login in the Telegram app sends no callback at all (oauth.telegram.org shows its account picker again). - Account link/unlink endpoints use
sensitiveSessionMiddlewareto reject revoked sessions even with cookie caching. Unlink also usesfreshSessionMiddleware. - Type guards (
validateTelegramAuthData,validateMiniAppData) run before processing. Don't skip validation. - Mini App endpoints are conditionally registered (
miniApp.enabled). Test both enabled and disabled paths. - OIDC provider is conditionally injected via
inithook (oidc.enabled). JWT verification inoidc-token.tsusesjoselibrary -- changes to JWKS fetching or token validation need scrutiny. - HTTP status codes: 400 (validation), 401 (auth), 403 (disabled), 404 (not found), 409 (conflict)
- Coverage thresholds are enforced in CI, but coverage does not prove assertions are useful. Add a test only for meaningful behavior not already protected.