From 67b09147eae3c70ce8756cc6cf1e9ba6ec530e57 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Thu, 21 May 2026 15:27:22 +0400 Subject: [PATCH] ppackage --- README.md | 156 ++++++-- TELEGRAM_USERAUTH_BACKEND.md | 43 +- angular.json | 3 + package-lock.json | 686 +++++++++++++++++++++++++++++++- package.json | 39 +- src/app/app.html | 60 +-- src/app/app.scss | 222 ----------- src/app/app.ts | 235 +---------- src/package/index.ts | 740 +++++++++++++++++++++++++++++++++++ 9 files changed, 1611 insertions(+), 573 deletions(-) create mode 100644 src/package/index.ts diff --git a/README.md b/README.md index 7d3b99c..ad9bc7e 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,114 @@ -# Telegram UserAuth UI +# telegram-userauth -Reusable Angular-hosted UI for Telegram login. +Reusable Telegram authentication package built as a web component for drop-in use across other repos. -The app now boots directly into the live `userauth` flow instead of a demo dialog. On load it: +It ships a custom element named `telegram-userauth` that: -- checks `GET /userauth/session` +- checks `GET /userauth/session` on startup - creates a QR session with `POST /userauth/qr/create` -- polls `GET /userauth/qr/poll?token=...` every 5 seconds -- falls back to session re-check polling if QR creation or polling fails +- polls `GET /userauth/qr/poll?token=...` every 5 seconds by default +- emits browser events when auth state changes, when login succeeds, and when backend calls fail +- works with same-origin APIs or a separate API origin through `apiBaseUrl` -## Run +## Install + +```bash +npm install telegram-userauth +``` + +## Use in another repo + +Register the custom element once in your app entrypoint: + +```ts +import { defineTelegramUserAuthElement } from 'telegram-userauth'; + +defineTelegramUserAuthElement(); +``` + +Render it anywhere in your app: + +```html + +``` + +Listen for successful login: + +```ts +const element = document.querySelector('telegram-userauth'); + +element?.addEventListener('userauth-authenticated', (event) => { + const detail = (event as CustomEvent).detail; + console.log('Authenticated session', detail.session); +}); +``` + +## Element attributes + +- `api-base-url`: backend origin, for example `https://api.example.com`. Leave empty for same-origin `/userauth/...`. +- `telegram-login-url`: optional direct Telegram button URL. If omitted, the button opens the deep link returned by `POST /userauth/qr/create`. +- `poll-interval-ms`: polling interval in milliseconds. Default: `5000`. +- `max-poll-attempts`: maximum QR poll attempts before the QR becomes expired. Default: `100`. +- `qr-size`: QR image size in pixels. Default: `220`. +- `title`: heading text. +- `description`: description text shown before authentication. +- `auto-start`: `true` by default. Set to `false` if the host app wants to call `element.start()` manually. + +## Custom events + +- `userauth-authenticated`: detail `{ session }` +- `userauth-statechange`: detail `{ state }` +- `userauth-error`: detail `{ message }` + +## Imperative API + +After selecting the element, the host app can call: + +- `await element.start()` +- `await element.refresh()` +- `element.currentSession` +- `element.options = { apiBaseUrl: 'https://api.example.com' }` + +## Example integrations + +### React / Next / Vite + +```ts +import { useEffect } from 'react'; +import { defineTelegramUserAuthElement } from 'telegram-userauth'; + +export function TelegramAuth() { + useEffect(() => { + defineTelegramUserAuthElement(); + }, []); + + return ; +} +``` + +### Angular host app + +```ts +import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core'; +import { defineTelegramUserAuthElement } from 'telegram-userauth'; + +defineTelegramUserAuthElement(); + +@Component({ + selector: 'app-auth-page', + template: '', + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class AuthPageComponent {} +``` + +## Local development ```bash npm start ``` -The dev server runs on port `4300`. +The local Angular app is only a preview host for the package and runs on port `4300`. ## Build @@ -23,45 +116,22 @@ The dev server runs on port `4300`. npm run build ``` -## Backend contract +This builds both the preview app and the publishable package. -This UI is intended to work against a reusable Telegram auth backend with these endpoints: +To build only the package output: -- `GET /userauth/session` -- `POST /userauth/qr/create` -- `GET /userauth/qr/poll?token=...` -- `POST /userauth/qr/confirm` -- `GET /userauth/telegram/callback` -- `POST /userauth/logout` -- `POST /usersession/{sessionId}` - -Expected authenticated session payload: - -```json -{ - "sessionId": "550e8400-e29b-41d4-a716-446655440000", - "telegramUserId": 123456789, - "username": "ivan_petrov", - "displayName": "Ivan Petrov", - "active": true, - "expiresAt": "2026-05-21T14:30:00Z" -} +```bash +npm run build:package ``` -Runtime expectations preserved by the UI: +## Backend contract -- QR polling every 5 seconds -- QR expiry after 100 checks on the frontend -- direct Telegram open button using the same deep link returned by QR creation -- fallback session re-check if QR creation fails +The backend requirements are documented in [TELEGRAM_USERAUTH_BACKEND.md](TELEGRAM_USERAUTH_BACKEND.md). -Cookie requirements expected by consumers: +Key expectations: -- name: `userauth_session` -- path: `/` -- `HttpOnly: true` -- `Secure: true` -- `SameSite: None` -- `MaxAge: 86400` - -Credentialed CORS is required on the backend. +- cookie name `userauth_session` +- credentialed CORS when frontend and backend are on different origins +- exact session response shape +- `POST /userauth/qr/create` returns `{ token, url }` +- `GET /userauth/qr/poll` returns `pending`, `confirmed`, or `expired` diff --git a/TELEGRAM_USERAUTH_BACKEND.md b/TELEGRAM_USERAUTH_BACKEND.md index 902464d..67cf8cd 100644 --- a/TELEGRAM_USERAUTH_BACKEND.md +++ b/TELEGRAM_USERAUTH_BACKEND.md @@ -29,6 +29,12 @@ There are two supported flows. 4. Backend sets `userauth_session` cookie and redirects back to the storefront. 5. Frontend calls `GET /userauth/session` and becomes authenticated. +Package note: + +- The published `telegram-userauth` package does not require this direct flow by default. +- If the host app provides `telegram-login-url`, the package button can use this flow. +- If `telegram-login-url` is omitted, the package button opens the `url` returned by `POST /userauth/qr/create`. + ### 2. QR login from desktop 1. Frontend opens dialog. @@ -36,7 +42,7 @@ There are two supported flows. 3. Backend returns `{ token, url }` where `url` is a Telegram deep link. 4. Frontend renders a QR from that URL. 5. User scans QR and bot calls `POST /userauth/qr/confirm`. -6. Frontend polls `GET /userauth/qr/poll?token=...` every 3 seconds. +6. Frontend polls `GET /userauth/qr/poll?token=...` every 5 seconds by default. 7. When status becomes `confirmed`, backend returns session payload and sets the cookie. 8. Frontend syncs local cart using `POST /usersession/{sessionId}`. @@ -99,7 +105,12 @@ Requirements: ### `GET /userauth/qr/poll?token={token}` -Called every 3 seconds until confirmation or expiration. +Called every 5 seconds by default until confirmation or expiration. + +Package note: + +- The reusable package exposes `poll-interval-ms` and defaults it to `5000`. +- Backend should not assume a stricter cadence than 5 seconds. Possible responses: @@ -184,6 +195,8 @@ Frontend behavior depends on this endpoint in two places: - initial auth check on app startup - fallback polling if QR token creation fails +This endpoint is also used by the package after successful direct-login redirects and during fallback retries. + Response `200`: ```json @@ -302,12 +315,30 @@ Do not use `*` for `Access-Control-Allow-Origin` together with credentials. ## Frontend Runtime Expectations -The current dialog behavior is fixed and should be preserved by backend responses. +The reusable package behavior should be preserved by backend responses. -- QR polling interval: every 3 seconds +- QR polling interval: every 5 seconds by default - QR expiration on frontend: after 100 checks -- If QR creation fails, frontend falls back to direct login URL and session polling -- After login, frontend closes the dialog and re-checks session +- If QR creation fails, frontend falls back to session polling +- Primary button opens `telegram-login-url` when the host app provides one +- Otherwise, primary button opens the deep link returned by `POST /userauth/qr/create` +- After login, frontend emits an authenticated event to the host app + +## Package Integration Notes + +The published package is a custom element named `telegram-userauth`. + +Expected host configuration: + +- `api-base-url` for separate backend origins, or empty for same-origin deployments +- optional `telegram-login-url` for dedicated direct-button flows +- optional `poll-interval-ms`, default `5000` + +Expected package events: + +- `userauth-authenticated` with `{ session }` +- `userauth-statechange` with `{ state }` +- `userauth-error` with `{ message }` ## Minimal Backend Checklist diff --git a/angular.json b/angular.json index 95906f4..2f53e3b 100644 --- a/angular.json +++ b/angular.json @@ -23,6 +23,9 @@ "browser": "src/main.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", + "allowedCommonJsDependencies": [ + "qrcode" + ], "assets": [ { "glob": "**/*", diff --git a/package-lock.json b/package-lock.json index 3bbd6f9..b3a0cd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { - "name": "auth-service", - "version": "0.0.0", + "name": "telegram-userauth", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "auth-service", - "version": "0.0.0", + "name": "telegram-userauth", + "version": "0.1.0", + "license": "UNLICENSED", "dependencies": { "@angular/common": "^21.0.0", "@angular/compiler": "^21.0.0", @@ -19,7 +20,10 @@ "@angular/build": "^21.0.4", "@angular/cli": "^21.0.4", "@angular/compiler-cli": "^21.0.0", + "@types/qrcode": "^1.5.6", "jsdom": "^27.1.0", + "qrcode": "^1.5.4", + "tsup": "^8.5.0", "typescript": "~5.9.2", "vitest": "^4.0.8" } @@ -3887,12 +3891,20 @@ "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", @@ -4048,6 +4060,19 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -4161,6 +4186,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -4284,6 +4316,22 @@ "dev": true, "license": "MIT" }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4294,6 +4342,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", @@ -4358,6 +4416,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001766", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", @@ -4569,6 +4637,33 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -4765,6 +4860,16 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -4793,6 +4898,13 @@ "node": ">=8" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -5256,6 +5368,32 @@ "url": "https://opencollective.com/express" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5818,6 +5956,16 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5932,6 +6080,26 @@ ], "license": "MIT" }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/listr2": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", @@ -6014,6 +6182,29 @@ "@lmdb/lmdb-win32-x64": "3.4.4" } }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -6379,6 +6570,19 @@ "node": ">= 18" } }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -6440,6 +6644,18 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -6787,6 +7003,35 @@ "license": "MIT", "optional": true }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", @@ -6800,6 +7045,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pacote": { "version": "21.0.4", "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.4.tgz", @@ -6909,6 +7164,16 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6991,6 +7256,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/piscina": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", @@ -7014,6 +7289,28 @@ "node": ">=16.20.0" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -7043,6 +7340,49 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/postcss-media-query-parser": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", @@ -7098,6 +7438,135 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/qrcode/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -7161,6 +7630,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -7171,6 +7650,13 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -7192,6 +7678,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -7460,6 +7956,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7822,6 +8325,29 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -7869,6 +8395,29 @@ "node": ">=18" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -7969,12 +8518,119 @@ "node": ">=20" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/tsup/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/tsup/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tuf-js": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", @@ -8019,6 +8675,13 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", @@ -8034,9 +8697,7 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/unique-filename": { "version": "5.0.0", @@ -8374,6 +9035,13 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/package.json b/package.json index aaebc18..3744292 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,17 @@ { - "name": "auth-service", - "version": "0.0.0", + "name": "telegram-userauth", + "version": "0.1.0", + "description": "Reusable Telegram user authentication web component for cross-repo integration.", "scripts": { "ng": "ng", "start": "ng serve --port 4300 --open", - "build": "ng build", + "build": "npm run build:app && npm run build:package", + "build:app": "ng build", + "build:package": "tsup src/package/index.ts --format esm,cjs --dts --clean --out-dir dist/package", "build:prod": "ng build --configuration production", "watch": "ng build --watch --configuration development", - "test": "ng test" + "test": "ng test", + "prepack": "npm run build:package" }, "prettier": { "printWidth": 100, @@ -21,7 +25,29 @@ } ] }, - "private": true, + "keywords": [ + "telegram", + "auth", + "web-component", + "userauth" + ], + "license": "UNLICENSED", + "main": "./dist/package/index.js", + "module": "./dist/package/index.mjs", + "types": "./dist/package/index.d.ts", + "exports": { + ".": { + "types": "./dist/package/index.d.ts", + "import": "./dist/package/index.mjs", + "require": "./dist/package/index.js" + } + }, + "files": [ + "dist/package", + "README.md", + "TELEGRAM_USERAUTH_BACKEND.md" + ], + "sideEffects": false, "packageManager": "npm@10.9.2", "dependencies": { "@angular/common": "^21.0.0", @@ -35,7 +61,10 @@ "@angular/build": "^21.0.4", "@angular/cli": "^21.0.4", "@angular/compiler-cli": "^21.0.0", + "@types/qrcode": "^1.5.6", "jsdom": "^27.1.0", + "qrcode": "^1.5.4", + "tsup": "^8.5.0", "typescript": "~5.9.2", "vitest": "^4.0.8" } diff --git a/src/app/app.html b/src/app/app.html index d403790..40a9460 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,59 +1 @@ -
- -
+ diff --git a/src/app/app.scss b/src/app/app.scss index 8938aab..d6f63ed 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -1,226 +1,4 @@ :host { - --bg-page: linear-gradient(135deg, #f4f7fb 0%, #e8eef4 100%); - --bg-card: #ffffff; - --text-primary: #1a1a1a; - --text-secondary: #666666; - --accent-color: #497671; - --accent-light: rgba(73, 118, 113, 0.1); - --telegram: #2aabee; - --telegram-hover: #229ed9; - --border: #e8e8e8; - --shadow: 0 20px 60px rgba(0, 0, 0, 0.15); - display: block; min-height: 100vh; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - color: var(--text-primary); - background: var(--bg-page); -} - -* { - box-sizing: border-box; -} - -.page { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - padding: 40px 32px; -} - -.login-card { - background: rgba(255, 255, 255, 0.72); - border: 1px solid rgba(255, 255, 255, 0.8); - border-radius: 28px; - padding: 32px 28px; - box-shadow: 0 18px 50px rgba(38, 52, 73, 0.12); - backdrop-filter: blur(14px); - max-width: 400px; - width: 100%; - text-align: center; -} - -.login-icon { - margin: 0 auto 16px; - width: 72px; - height: 72px; - border-radius: 50%; - background: var(--accent-light); - color: var(--accent-color); - display: flex; - align-items: center; - justify-content: center; -} - -h1 { - margin: 0 0 8px; - font-size: 28px; - line-height: 1.1; -} - -.status-copy { - margin: 0 0 24px; - font-size: 14px; - color: var(--text-secondary); - line-height: 1.5; -} - -.telegram-btn { - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - width: 100%; - padding: 14px 24px; - border: none; - border-radius: 12px; - background: var(--telegram); - color: #fff; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; -} - -.telegram-btn:enabled:hover { - background: var(--telegram-hover); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3); -} - -.telegram-btn:disabled { - opacity: 0.7; - cursor: wait; -} - -.secondary-btn { - width: 100%; - padding: 12px 16px; - border: 1px solid #cfd8e3; - border-radius: 12px; - background: #fff; - color: var(--text-primary); - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: 0.2s ease; -} - -.secondary-btn:hover { - border-color: var(--accent-color); - color: var(--accent-color); -} - -.qr-section { - display: flex; - justify-content: center; - margin: 20px 0 16px; -} - -.qr-container { - display: inline-flex; - padding: 12px; - background: #fff; - border-radius: 12px; - border: 1px solid var(--border); -} - -.qr-container img { - display: block; - border-radius: 4px; -} - -.qr-loading { - align-items: center; - justify-content: center; - width: 244px; - height: 244px; -} - -.qr-loading .spinner { - width: 32px; - height: 32px; - border: 3px solid #e0e0e0; - border-top-color: var(--accent-color); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -.qr-expired { - flex-direction: column; - align-items: center; - justify-content: center; - gap: 8px; - width: 244px; - height: 244px; - cursor: pointer; - color: #999; - transition: color 0.2s ease; - font: inherit; -} - -.qr-expired:hover { - color: var(--accent-color); -} - -.qr-expired span { - font-size: 13px; -} - -.tg-icon { - flex-shrink: 0; -} - -.session-card { - display: grid; - gap: 8px; - padding: 18px; - border-radius: 16px; - background: rgba(255, 255, 255, 0.8); - border: 1px solid #e2e8f0; - text-align: left; -} - -.session-card strong { - font-size: 18px; -} - -.session-card span { - font-size: 14px; - color: var(--text-secondary); -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -@media (max-width: 640px) { - .page { - padding: 24px 16px 32px; - } - - .login-card { - padding: 24px 20px; - border-radius: 20px; - } -} - -@media (max-width: 480px) { - h1 { - font-size: 24px; - } - - .qr-container img { - width: 180px; - height: 180px; - } - - .qr-loading, - .qr-expired { - width: 204px; - height: 204px; - } } diff --git a/src/app/app.ts b/src/app/app.ts index d0b4abf..023d2c9 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,235 +1,12 @@ -import { DatePipe } from '@angular/common'; -import { HttpClient } from '@angular/common/http'; -import { Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core'; -import { firstValueFrom } from 'rxjs'; +import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core'; +import { defineTelegramUserAuthElement } from '../package'; -type AuthState = 'loading' | 'ready' | 'checking' | 'expired' | 'error' | 'authenticated'; - -interface QrCreateResponse { - token: string; - url: string; -} - -interface AuthSession { - sessionId: string; - telegramUserId: number; - username: string | null; - displayName: string; - active: boolean; - expiresAt: string; -} - -interface QrPollResponse { - status: 'pending' | 'confirmed' | 'expired'; - session?: AuthSession; -} - -const POLL_INTERVAL_MS = 5000; -const MAX_POLL_ATTEMPTS = 100; +defineTelegramUserAuthElement(); @Component({ selector: 'app-root', - imports: [DatePipe], templateUrl: './app.html', - styleUrl: './app.scss' + styleUrl: './app.scss', + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) -export class App implements OnInit, OnDestroy { - private readonly http = inject(HttpClient); - private pollTimer: number | null = null; - - protected readonly state = signal('loading'); - protected readonly session = signal(null); - protected readonly telegramUrl = signal(''); - protected readonly qrToken = signal(''); - protected readonly errorMessage = signal(''); - protected readonly pollAttempts = signal(0); - protected readonly qrImageUrl = computed(() => { - const telegramUrl = this.telegramUrl(); - if (!telegramUrl) { - return ''; - } - - return `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(telegramUrl)}`; - }); - protected readonly statusMessage = computed(() => { - const session = this.session(); - - switch (this.state()) { - case 'loading': - return 'Creating your Telegram login session.'; - case 'checking': - return `Waiting for confirmation. Checking every ${POLL_INTERVAL_MS / 1000} seconds.`; - case 'expired': - return 'This QR code expired. Refresh to generate a new one.'; - case 'error': - return this.errorMessage() || 'Unable to reach the auth backend.'; - case 'authenticated': - return session ? `${session.displayName} is authenticated.` : 'Authenticated.'; - default: - return `Open Telegram or scan the QR code. The page checks every ${POLL_INTERVAL_MS / 1000} seconds.`; - } - }); - - async ngOnInit(): Promise { - await this.initializeAuth(); - } - - ngOnDestroy(): void { - this.stopPolling(); - } - - protected async refreshQr(): Promise { - await this.startQrFlow(); - } - - protected openTelegram(): void { - const telegramUrl = this.telegramUrl(); - - if (!telegramUrl) { - void this.startQrFlow(); - return; - } - - window.location.href = telegramUrl; - } - - private async initializeAuth(): Promise { - this.stopPolling(); - this.state.set('checking'); - - try { - const session = await firstValueFrom( - this.http.get(this.buildApiUrl('/userauth/session'), { - withCredentials: true - }) - ); - - if (session?.active) { - this.handleAuthenticated(session); - return; - } - } catch { - // Non-200 means unauthenticated for this contract. - } - - await this.startQrFlow(); - } - - private async startQrFlow(): Promise { - this.stopPolling(); - this.state.set('loading'); - this.errorMessage.set(''); - this.telegramUrl.set(''); - this.qrToken.set(''); - this.pollAttempts.set(0); - - try { - const response = await firstValueFrom( - this.http.post( - this.buildApiUrl('/userauth/qr/create'), - {}, - { withCredentials: true } - ) - ); - - if (!response?.token || !response?.url) { - throw new Error('Invalid QR create response'); - } - - this.qrToken.set(response.token); - this.telegramUrl.set(response.url); - this.state.set('ready'); - this.startQrPolling(response.token); - } catch { - this.state.set('error'); - this.errorMessage.set('Unable to create a QR session. Retrying session lookup in the background.'); - this.startSessionFallbackPolling(); - } - } - - private startQrPolling(token: string): void { - this.stopPolling(); - this.pollTimer = window.setInterval(() => { - void this.pollQrStatus(token); - }, POLL_INTERVAL_MS); - } - - private startSessionFallbackPolling(): void { - this.stopPolling(); - this.pollTimer = window.setInterval(() => { - void this.checkSessionFallback(); - }, POLL_INTERVAL_MS); - } - - private async pollQrStatus(token: string): Promise { - const attempt = this.pollAttempts() + 1; - this.pollAttempts.set(attempt); - - if (attempt >= MAX_POLL_ATTEMPTS) { - this.stopPolling(); - this.state.set('expired'); - return; - } - - this.state.set('checking'); - - try { - const response = await firstValueFrom( - this.http.get(this.buildApiUrl(`/userauth/qr/poll?token=${encodeURIComponent(token)}`), { - withCredentials: true - }) - ); - - if (response.status === 'confirmed' && response.session) { - this.handleAuthenticated(response.session); - return; - } - - if (response.status === 'expired') { - this.stopPolling(); - this.state.set('expired'); - return; - } - - this.state.set('ready'); - } catch { - this.stopPolling(); - this.state.set('error'); - this.errorMessage.set('Polling failed. Retrying session lookup in the background.'); - this.startSessionFallbackPolling(); - } - } - - private async checkSessionFallback(): Promise { - try { - const session = await firstValueFrom( - this.http.get(this.buildApiUrl('/userauth/session'), { - withCredentials: true - }) - ); - - if (session?.active) { - this.handleAuthenticated(session); - } - } catch { - // Keep fallback polling until the user refreshes QR or the backend becomes available. - } - } - - private handleAuthenticated(session: AuthSession): void { - this.stopPolling(); - this.session.set(session); - this.state.set('authenticated'); - } - - private stopPolling(): void { - if (this.pollTimer !== null) { - window.clearInterval(this.pollTimer); - this.pollTimer = null; - } - } - - private buildApiUrl(path: string): string { - return path; - } -} +export class App {} diff --git a/src/package/index.ts b/src/package/index.ts new file mode 100644 index 0000000..a53d86f --- /dev/null +++ b/src/package/index.ts @@ -0,0 +1,740 @@ +import QRCode from 'qrcode'; + +export type AuthState = 'loading' | 'ready' | 'checking' | 'expired' | 'error' | 'authenticated'; + +export interface TelegramUserAuthSession { + sessionId: string; + telegramUserId: number; + username: string | null; + displayName: string; + active: boolean; + expiresAt: string; +} + +export interface TelegramUserAuthConfig { + apiBaseUrl: string; + telegramLoginUrl?: string; + pollIntervalMs: number; + maxPollAttempts: number; + qrSize: number; + title: string; + description: string; + autoStart: boolean; +} + +export interface TelegramUserAuthAuthenticatedDetail { + session: TelegramUserAuthSession; +} + +export interface TelegramUserAuthErrorDetail { + message: string; +} + +export interface TelegramUserAuthStateChangeDetail { + state: AuthState; +} + +interface QrCreateResponse { + token: string; + url: string; +} + +interface QrPollResponse { + status: 'pending' | 'confirmed' | 'expired'; + session?: TelegramUserAuthSession; +} + +const DEFAULT_TAG_NAME = 'telegram-userauth'; +const DEFAULT_CONFIG: TelegramUserAuthConfig = { + apiBaseUrl: '', + telegramLoginUrl: undefined, + pollIntervalMs: 5000, + maxPollAttempts: 100, + qrSize: 220, + title: 'Continue With Telegram', + description: 'Open Telegram or scan the QR code to authenticate.', + autoStart: true +}; + +const COMPONENT_STYLES = ` + :host { + --bg-page: linear-gradient(135deg, #f4f7fb 0%, #e8eef4 100%); + --bg-card: rgba(255, 255, 255, 0.78); + --text-primary: #1a1a1a; + --text-secondary: #666666; + --accent-color: #497671; + --accent-light: rgba(73, 118, 113, 0.1); + --telegram: #2aabee; + --telegram-hover: #229ed9; + --border: #e8e8e8; + --shadow: 0 20px 60px rgba(0, 0, 0, 0.15); + + display: block; + min-height: 100vh; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: var(--text-primary); + background: var(--bg-page); + } + + * { + box-sizing: border-box; + } + + .page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 32px; + } + + .login-card { + background: var(--bg-card); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 28px; + padding: 32px 28px; + box-shadow: 0 18px 50px rgba(38, 52, 73, 0.12); + backdrop-filter: blur(14px); + max-width: 400px; + width: 100%; + text-align: center; + } + + .login-icon { + margin: 0 auto 16px; + width: 72px; + height: 72px; + border-radius: 50%; + background: var(--accent-light); + color: var(--accent-color); + display: flex; + align-items: center; + justify-content: center; + } + + h1 { + margin: 0 0 8px; + font-size: 28px; + line-height: 1.1; + } + + .status-copy { + margin: 0 0 24px; + font-size: 14px; + color: var(--text-secondary); + line-height: 1.5; + } + + .telegram-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + padding: 14px 24px; + border: none; + border-radius: 12px; + background: var(--telegram); + color: #fff; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + } + + .telegram-btn:enabled:hover { + background: var(--telegram-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3); + } + + .telegram-btn:disabled { + opacity: 0.7; + cursor: wait; + } + + .secondary-btn { + width: 100%; + padding: 12px 16px; + border: 1px solid #cfd8e3; + border-radius: 12px; + background: #fff; + color: var(--text-primary); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: 0.2s ease; + } + + .secondary-btn:hover { + border-color: var(--accent-color); + color: var(--accent-color); + } + + .tg-icon { + flex-shrink: 0; + } + + .qr-section { + display: flex; + justify-content: center; + margin: 20px 0 16px; + } + + .qr-container { + display: inline-flex; + padding: 12px; + background: #fff; + border-radius: 12px; + border: 1px solid var(--border); + } + + .qr-container img { + display: block; + border-radius: 4px; + } + + .qr-loading { + align-items: center; + justify-content: center; + width: 244px; + height: 244px; + } + + .spinner { + width: 32px; + height: 32px; + border: 3px solid #e0e0e0; + border-top-color: var(--accent-color); + border-radius: 50%; + animation: spin 0.8s linear infinite; + } + + .qr-expired { + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + width: 244px; + height: 244px; + cursor: pointer; + color: #999; + transition: color 0.2s ease; + font: inherit; + } + + .qr-expired:hover { + color: var(--accent-color); + } + + .qr-expired span { + font-size: 13px; + } + + .session-card { + display: grid; + gap: 8px; + padding: 18px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.8); + border: 1px solid #e2e8f0; + text-align: left; + } + + .session-card strong { + font-size: 18px; + } + + .session-card span { + font-size: 14px; + color: var(--text-secondary); + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + @media (max-width: 640px) { + .page { + padding: 24px 16px 32px; + } + + .login-card { + padding: 24px 20px; + border-radius: 20px; + } + } + + @media (max-width: 480px) { + h1 { + font-size: 24px; + } + + .qr-container img { + width: 180px; + height: 180px; + } + + .qr-loading, + .qr-expired { + width: 204px; + height: 204px; + } + } +`; + +export class TelegramUserAuthElement extends HTMLElement { + static observedAttributes = [ + 'api-base-url', + 'telegram-login-url', + 'poll-interval-ms', + 'max-poll-attempts', + 'qr-size', + 'title', + 'description', + 'auto-start' + ]; + + private readonly root: ShadowRoot; + private pollTimer: number | null = null; + private started = false; + private optionOverrides: Partial = {}; + + private config: TelegramUserAuthConfig = { ...DEFAULT_CONFIG }; + private state: AuthState = 'loading'; + private session: TelegramUserAuthSession | null = null; + private qrToken = ''; + private qrUrl = ''; + private qrImageUrl = ''; + private errorMessage = ''; + private pollAttempts = 0; + + constructor() { + super(); + this.root = this.attachShadow({ mode: 'open' }); + } + + connectedCallback(): void { + this.applyConfig(); + this.render(); + + if (!this.started && this.config.autoStart) { + this.started = true; + void this.initializeAuth(); + } + } + + disconnectedCallback(): void { + this.stopPolling(); + } + + attributeChangedCallback(): void { + const previousBaseUrl = this.config.apiBaseUrl; + const previousTelegramLoginUrl = this.config.telegramLoginUrl; + + this.applyConfig(); + this.render(); + + if ( + this.started && + this.config.autoStart && + (previousBaseUrl !== this.config.apiBaseUrl || previousTelegramLoginUrl !== this.config.telegramLoginUrl) + ) { + void this.initializeAuth(); + } + } + + set options(value: Partial) { + this.optionOverrides = { ...this.optionOverrides, ...value }; + this.applyConfig(); + this.render(); + + if (this.isConnected && this.config.autoStart) { + if (!this.started) { + this.started = true; + } + + void this.initializeAuth(); + } + } + + get options(): TelegramUserAuthConfig { + return { ...this.config }; + } + + get currentSession(): TelegramUserAuthSession | null { + return this.session; + } + + async start(): Promise { + this.started = true; + await this.initializeAuth(); + } + + async refresh(): Promise { + await this.startQrFlow(); + } + + private applyConfig(): void { + this.config = { + ...DEFAULT_CONFIG, + ...this.readAttributeConfig(), + ...this.optionOverrides + }; + } + + private readAttributeConfig(): Partial { + return { + apiBaseUrl: this.getAttribute('api-base-url') ?? DEFAULT_CONFIG.apiBaseUrl, + telegramLoginUrl: this.getAttribute('telegram-login-url') ?? undefined, + pollIntervalMs: this.getNumberAttribute('poll-interval-ms', DEFAULT_CONFIG.pollIntervalMs), + maxPollAttempts: this.getNumberAttribute('max-poll-attempts', DEFAULT_CONFIG.maxPollAttempts), + qrSize: this.getNumberAttribute('qr-size', DEFAULT_CONFIG.qrSize), + title: this.getAttribute('title') ?? DEFAULT_CONFIG.title, + description: this.getAttribute('description') ?? DEFAULT_CONFIG.description, + autoStart: this.getBooleanAttribute('auto-start', DEFAULT_CONFIG.autoStart) + }; + } + + private getNumberAttribute(name: string, fallback: number): number { + const value = this.getAttribute(name); + if (!value) { + return fallback; + } + + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; + } + + private getBooleanAttribute(name: string, fallback: boolean): boolean { + const value = this.getAttribute(name); + if (value === null) { + return fallback; + } + + return value !== 'false'; + } + + private async initializeAuth(): Promise { + this.stopPolling(); + this.setState('checking'); + + try { + const session = await this.fetchSession(); + if (session.active) { + this.handleAuthenticated(session); + return; + } + } catch { + // Non-200 means unauthenticated in this contract. + } + + await this.startQrFlow(); + } + + private async startQrFlow(): Promise { + this.stopPolling(); + this.session = null; + this.qrToken = ''; + this.qrUrl = ''; + this.qrImageUrl = ''; + this.errorMessage = ''; + this.pollAttempts = 0; + this.setState('loading'); + + try { + const response = await this.fetchJson(this.buildApiUrl('/userauth/qr/create'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: '{}', + credentials: 'include' + }); + + if (!response?.token || !response?.url) { + throw new Error('Invalid QR create response'); + } + + this.qrToken = response.token; + this.qrUrl = response.url; + this.qrImageUrl = await QRCode.toDataURL(response.url, { + width: this.config.qrSize, + margin: 1 + }); + this.setState('ready'); + this.startQrPolling(response.token); + } catch { + this.errorMessage = 'Unable to create a QR session. Retrying session lookup in the background.'; + this.setState('error'); + this.dispatchPackageError(this.errorMessage); + this.startSessionFallbackPolling(); + } + } + + private startQrPolling(token: string): void { + this.stopPolling(); + this.pollTimer = window.setInterval(() => { + void this.pollQrStatus(token); + }, this.config.pollIntervalMs); + } + + private startSessionFallbackPolling(): void { + this.stopPolling(); + this.pollTimer = window.setInterval(() => { + void this.checkSessionFallback(); + }, this.config.pollIntervalMs); + } + + private async pollQrStatus(token: string): Promise { + this.pollAttempts += 1; + + if (this.pollAttempts >= this.config.maxPollAttempts) { + this.stopPolling(); + this.setState('expired'); + return; + } + + this.setState('checking'); + + try { + const response = await this.fetchJson( + this.buildApiUrl(`/userauth/qr/poll?token=${encodeURIComponent(token)}`), + { + credentials: 'include' + } + ); + + if (response.status === 'confirmed' && response.session) { + this.handleAuthenticated(response.session); + return; + } + + if (response.status === 'expired') { + this.stopPolling(); + this.setState('expired'); + return; + } + + this.setState('ready'); + } catch { + this.stopPolling(); + this.errorMessage = 'Polling failed. Retrying session lookup in the background.'; + this.setState('error'); + this.dispatchPackageError(this.errorMessage); + this.startSessionFallbackPolling(); + } + } + + private async checkSessionFallback(): Promise { + try { + const session = await this.fetchSession(); + if (session.active) { + this.handleAuthenticated(session); + } + } catch { + // Continue fallback polling until the backend becomes reachable or the user refreshes. + } + } + + private async fetchSession(): Promise { + return this.fetchJson(this.buildApiUrl('/userauth/session'), { + credentials: 'include' + }); + } + + private handleAuthenticated(session: TelegramUserAuthSession): void { + this.stopPolling(); + this.session = session; + this.setState('authenticated'); + this.dispatchEvent( + new CustomEvent('userauth-authenticated', { + detail: { session }, + bubbles: true, + composed: true + }) + ); + } + + private setState(state: AuthState): void { + this.state = state; + this.render(); + this.dispatchEvent( + new CustomEvent('userauth-statechange', { + detail: { state }, + bubbles: true, + composed: true + }) + ); + } + + private dispatchPackageError(message: string): void { + this.dispatchEvent( + new CustomEvent('userauth-error', { + detail: { message }, + bubbles: true, + composed: true + }) + ); + } + + private stopPolling(): void { + if (this.pollTimer !== null) { + window.clearInterval(this.pollTimer); + this.pollTimer = null; + } + } + + private async openTelegram(): Promise { + const destination = this.config.telegramLoginUrl || this.qrUrl; + + if (!destination) { + await this.startQrFlow(); + return; + } + + window.location.href = destination; + } + + private buildApiUrl(path: string): string { + const baseUrl = this.config.apiBaseUrl.replace(/\/+$/, ''); + return baseUrl ? `${baseUrl}${path}` : path; + } + + private async fetchJson(input: RequestInfo | URL, init: RequestInit): Promise { + const response = await fetch(input, { + ...init, + credentials: 'include', + headers: { + Accept: 'application/json', + ...(init.headers ?? {}) + } + }); + + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}`); + } + + return (await response.json()) as T; + } + + private render(): void { + const sessionContent = this.session + ? ` +

Authenticated

+

${this.escapeHtml(this.getStatusMessage())}

+
+ ${this.escapeHtml(this.session.displayName)} + ${this.escapeHtml(this.session.username ? `@${this.session.username}` : `Telegram user ${this.session.telegramUserId}`)} + ${this.escapeHtml(`Session ${this.session.sessionId}`)} + ${this.escapeHtml(`Expires ${new Date(this.session.expiresAt).toLocaleString()}`)} +
+ ` + : ` +

${this.escapeHtml(this.config.title)}

+

${this.escapeHtml(this.getStatusMessage())}

+ +
${this.renderQrSection()}
+ + `; + + this.root.innerHTML = ` + +
+ +
+ `; + + this.root.querySelector('[data-action="open"]')?.addEventListener('click', () => { + void this.openTelegram(); + }); + + this.root.querySelector('[data-action="refresh"]')?.addEventListener('click', () => { + void this.startQrFlow(); + }); + } + + private renderQrSection(): string { + if (this.state === 'loading') { + return '
'; + } + + if (this.state === 'expired' || this.state === 'error') { + return ` + + `; + } + + if (this.qrImageUrl) { + return `
Telegram login QR code
`; + } + + return ''; + } + + private getStatusMessage(): string { + switch (this.state) { + case 'loading': + return 'Creating your Telegram login session.'; + case 'checking': + return `Waiting for confirmation. Checking every ${this.config.pollIntervalMs / 1000} seconds.`; + case 'expired': + return 'This QR code expired. Refresh to generate a new one.'; + case 'error': + return this.errorMessage || 'Unable to reach the auth backend.'; + case 'authenticated': + return this.session ? `${this.session.displayName} is authenticated.` : 'Authenticated.'; + default: + return this.config.description; + } + } + + private escapeHtml(value: string): string { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + } + + private escapeAttribute(value: string): string { + return this.escapeHtml(value); + } +} + +export function defineTelegramUserAuthElement(tagName = DEFAULT_TAG_NAME): typeof TelegramUserAuthElement { + const existing = customElements.get(tagName); + if (!existing) { + customElements.define(tagName, TelegramUserAuthElement); + return TelegramUserAuthElement; + } + + return existing as typeof TelegramUserAuthElement; +} + +declare global { + interface HTMLElementTagNameMap { + 'telegram-userauth': TelegramUserAuthElement; + } +}