Compare commits
2 Commits
96afd4140e
...
67b09147ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67b09147ea | ||
|
|
d21056bf96 |
159
README.md
159
README.md
@@ -1,22 +1,114 @@
|
||||
# Telegram UserAuth UI
|
||||
# telegram-userauth
|
||||
|
||||
Reusable Angular-hosted UI for the Telegram login dialog.
|
||||
Reusable Telegram authentication package built as a web component for drop-in use across other repos.
|
||||
|
||||
The app now contains a single standalone page based on the extracted dialog design. It preserves the same visual states and the same state-switcher behavior from the provided HTML:
|
||||
It ships a custom element named `telegram-userauth` that:
|
||||
|
||||
- `ready`
|
||||
- `loading`
|
||||
- `checking`
|
||||
- `expired`
|
||||
- `error`
|
||||
- checks `GET /userauth/session` on startup
|
||||
- creates a QR session with `POST /userauth/qr/create`
|
||||
- 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
|
||||
<telegram-userauth api-base-url="https://api.example.com"></telegram-userauth>
|
||||
```
|
||||
|
||||
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 <telegram-userauth api-base-url="https://api.example.com" />;
|
||||
}
|
||||
```
|
||||
|
||||
### Angular host app
|
||||
|
||||
```ts
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';
|
||||
import { defineTelegramUserAuthElement } from 'telegram-userauth';
|
||||
|
||||
defineTelegramUserAuthElement();
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth-page',
|
||||
template: '<telegram-userauth api-base-url="https://api.example.com"></telegram-userauth>',
|
||||
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
|
||||
|
||||
@@ -24,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 3 seconds
|
||||
- QR expiry after 100 checks on the frontend
|
||||
- direct Telegram login button support
|
||||
- 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`
|
||||
|
||||
358
TELEGRAM_USERAUTH_BACKEND.md
Normal file
358
TELEGRAM_USERAUTH_BACKEND.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# Telegram UserAuth Backend Contract
|
||||
|
||||
This document extracts the existing Telegram login flow into a repo-neutral contract for reuse in other projects.
|
||||
|
||||
The UI behavior, payloads, polling cadence, and session model stay the same. Only route names and cookie naming are generalized.
|
||||
|
||||
## Endpoint Renaming
|
||||
|
||||
| Current app contract | Reusable contract |
|
||||
|---|---|
|
||||
| `GET /auth/session` | `GET /userauth/session` |
|
||||
| `POST /auth/qr/create` | `POST /userauth/qr/create` |
|
||||
| `GET /auth/qr/poll?token=...` | `GET /userauth/qr/poll?token=...` |
|
||||
| `POST /auth/qr/confirm` | `POST /userauth/qr/confirm` |
|
||||
| `GET /auth/telegram/callback` | `GET /userauth/telegram/callback` |
|
||||
| `POST /auth/logout` | `POST /userauth/logout` |
|
||||
| `POST /websession/{sessionId}` | `POST /usersession/{sessionId}` |
|
||||
| Cookie `dx_session` | Cookie `userauth_session` |
|
||||
|
||||
## Flow Summary
|
||||
|
||||
There are two supported flows.
|
||||
|
||||
### 1. Direct login from button
|
||||
|
||||
1. Frontend opens `https://t.me/{botUsername}?start=auth_{callbackUrl}`.
|
||||
2. Telegram bot creates a session and sends the user a login button.
|
||||
3. The button points to `GET /userauth/telegram/callback?token={sessionId}`.
|
||||
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.
|
||||
2. Frontend calls `POST /userauth/qr/create`.
|
||||
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 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}`.
|
||||
|
||||
## Session Shape
|
||||
|
||||
The frontend expects this exact response shape for the authenticated session.
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"telegramUserId": 123456789,
|
||||
"username": "ivan_petrov",
|
||||
"displayName": "Ivan Petrov",
|
||||
"active": true,
|
||||
"expiresAt": "2026-05-21T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `sessionId` | string | yes | Session identifier used in cart sync |
|
||||
| `telegramUserId` | number | yes | Telegram user ID |
|
||||
| `username` | string or null | no | Telegram username |
|
||||
| `displayName` | string | yes | User-facing full name |
|
||||
| `active` | boolean | yes | `false` means expired session |
|
||||
| `expiresAt` | ISO 8601 string | yes | Used by frontend refresh scheduling |
|
||||
|
||||
Recommended TTL:
|
||||
|
||||
- Session TTL: 24 hours
|
||||
- QR token TTL: 5 minutes
|
||||
|
||||
## HTTP Contract
|
||||
|
||||
### `POST /userauth/qr/create`
|
||||
|
||||
Creates a one-time QR login token when the dialog opens.
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
Response `200`:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "dG9rZW4tYWJjMTIz",
|
||||
"url": "https://t.me/userauth_bot?start=login_dG9rZW4tYWJjMTIz"
|
||||
}
|
||||
```
|
||||
|
||||
Requirements:
|
||||
|
||||
- Generate a cryptographically secure token.
|
||||
- Save token with status `pending`.
|
||||
- Return a Telegram deep link in `url`.
|
||||
- Rate limit to 5 requests per minute per IP.
|
||||
|
||||
### `GET /userauth/qr/poll?token={token}`
|
||||
|
||||
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:
|
||||
|
||||
Pending:
|
||||
|
||||
```json
|
||||
{ "status": "pending" }
|
||||
```
|
||||
|
||||
Confirmed:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "confirmed",
|
||||
"session": {
|
||||
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"telegramUserId": 123456789,
|
||||
"username": "ivan_petrov",
|
||||
"displayName": "Ivan Petrov",
|
||||
"active": true,
|
||||
"expiresAt": "2026-05-21T14:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expired:
|
||||
|
||||
```json
|
||||
{ "status": "expired" }
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- If confirmed, set cookie `userauth_session` in the response.
|
||||
- Delete or invalidate the QR token after the first successful confirmed poll.
|
||||
- If token is unknown or expired, return `status: "expired"`.
|
||||
|
||||
### `POST /userauth/qr/confirm`
|
||||
|
||||
Internal endpoint called by the Telegram bot after the user scans the QR code.
|
||||
|
||||
Required header:
|
||||
|
||||
```text
|
||||
X-Bot-Secret: <shared secret between bot and backend>
|
||||
```
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "dG9rZW4tYWJjMTIz",
|
||||
"telegram_user": {
|
||||
"id": 123456789,
|
||||
"first_name": "Ivan",
|
||||
"last_name": "Petrov",
|
||||
"username": "ivan_petrov"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response `200`:
|
||||
|
||||
```json
|
||||
{ "status": "ok" }
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- Validate `X-Bot-Secret`.
|
||||
- Validate token exists and is still `pending`.
|
||||
- Create a user session.
|
||||
- Store session ID on the QR token.
|
||||
- Mark QR token as `confirmed`.
|
||||
|
||||
### `GET /userauth/session`
|
||||
|
||||
Returns the currently active session based on the cookie.
|
||||
|
||||
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
|
||||
{
|
||||
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"telegramUserId": 123456789,
|
||||
"username": "ivan_petrov",
|
||||
"displayName": "Ivan Petrov",
|
||||
"active": true,
|
||||
"expiresAt": "2026-05-21T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Error handling:
|
||||
|
||||
- Any non-200 response is treated by the frontend as unauthenticated.
|
||||
|
||||
### `GET /userauth/telegram/callback?token={sessionId}`
|
||||
|
||||
Used for direct Telegram login from the primary button flow.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Read the `token` query param.
|
||||
- Resolve it to a valid active session.
|
||||
- Set cookie `userauth_session`.
|
||||
- Redirect user to the storefront URL.
|
||||
|
||||
### `POST /userauth/logout`
|
||||
|
||||
Clears the backend session and expires the cookie.
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
Response `200`:
|
||||
|
||||
```json
|
||||
{ "message": "ok" }
|
||||
```
|
||||
|
||||
### `POST /usersession/{sessionId}`
|
||||
|
||||
Synchronizes local cart immediately after successful login.
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"itemID": 123,
|
||||
"quantity": 2,
|
||||
"colour": "#ff0000",
|
||||
"size": "XL",
|
||||
"price": 1500
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- This payload is unchanged from the existing implementation.
|
||||
- `price` is already discounted on the frontend side.
|
||||
- The frontend skips the call if cart is empty.
|
||||
|
||||
## Telegram Deep Link Format
|
||||
|
||||
Direct login link format:
|
||||
|
||||
```text
|
||||
https://t.me/{botUsername}?start=auth_{urlEncodedCallbackUrl}
|
||||
```
|
||||
|
||||
QR login link format:
|
||||
|
||||
```text
|
||||
https://t.me/{botUsername}?start=login_{qrToken}
|
||||
```
|
||||
|
||||
Important limit:
|
||||
|
||||
- Telegram limits the `start` payload to 64 characters.
|
||||
- A base64url encoding of 32 random bytes plus `login_` fits safely.
|
||||
|
||||
## Cookie Requirements
|
||||
|
||||
Use these cookie settings for the frontend to work correctly across site and API origins.
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Name | `userauth_session` |
|
||||
| Path | `/` |
|
||||
| HttpOnly | `true` |
|
||||
| Secure | `true` |
|
||||
| SameSite | `None` |
|
||||
| MaxAge | `86400` |
|
||||
| Domain | your shared parent domain, for example `.example.com` |
|
||||
|
||||
## CORS Requirements
|
||||
|
||||
Because the frontend sends credentials, backend must return an explicit origin.
|
||||
|
||||
Required headers:
|
||||
|
||||
```text
|
||||
Access-Control-Allow-Origin: https://your-frontend.example
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||||
Access-Control-Allow-Headers: Content-Type
|
||||
```
|
||||
|
||||
Do not use `*` for `Access-Control-Allow-Origin` together with credentials.
|
||||
|
||||
## Frontend Runtime Expectations
|
||||
|
||||
The reusable package behavior should be preserved by backend responses.
|
||||
|
||||
- QR polling interval: every 5 seconds by default
|
||||
- QR expiration on frontend: after 100 checks
|
||||
- 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
|
||||
|
||||
- Implement all six `userauth` endpoints and the `usersession` sync endpoint.
|
||||
- Store sessions for 24 hours.
|
||||
- Store QR tokens for 5 minutes.
|
||||
- Protect `POST /userauth/qr/confirm` with `X-Bot-Secret`.
|
||||
- Set `userauth_session` cookie on confirmed QR poll and direct callback.
|
||||
- Return the exact session JSON shape.
|
||||
- Support credentialed CORS.
|
||||
|
||||
## Bot Checklist
|
||||
|
||||
- Handle `/start login_{token}` and call `POST /userauth/qr/confirm`.
|
||||
- Handle `/start auth_{callbackUrl}` and provide a button that opens the callback URL.
|
||||
- Send success and expiration messages back to the user.
|
||||
- Share the same `X-Bot-Secret` value with backend.
|
||||
@@ -23,6 +23,9 @@
|
||||
"browser": "src/main.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"allowedCommonJsDependencies": [
|
||||
"qrcode"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
|
||||
686
package-lock.json
generated
686
package-lock.json
generated
@@ -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",
|
||||
|
||||
39
package.json
39
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"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideBrowserGlobalErrorListeners()]
|
||||
providers: [provideBrowserGlobalErrorListeners(), provideHttpClient()]
|
||||
};
|
||||
|
||||
121
src/app/app.html
121
src/app/app.html
@@ -1,120 +1 @@
|
||||
<main class="page">
|
||||
<section class="panel info">
|
||||
<h1>Telegram Login Dialog</h1>
|
||||
<p>
|
||||
Standalone extraction of the current login popup: same layout, same visual states,
|
||||
same QR flow, but with reusable neutral endpoint names for moving into a separate repo.
|
||||
</p>
|
||||
|
||||
<div class="state-switcher" aria-label="Dialog state switcher">
|
||||
@for (dialogState of states; track dialogState) {
|
||||
<button
|
||||
[class.active]="state() === dialogState"
|
||||
[attr.data-state-btn]="dialogState"
|
||||
type="button"
|
||||
(click)="setState(dialogState)"
|
||||
>
|
||||
{{ dialogState === 'ready' ? 'Ready' : dialogState === 'loading' ? 'QR Loading' : dialogState === 'checking' ? 'Checking' : dialogState === 'expired' ? 'Expired' : 'Fallback' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="api-grid">
|
||||
<div class="api-card">
|
||||
<strong>Start QR session</strong>
|
||||
<code>POST /userauth/qr/create</code>
|
||||
<p>Returns a one-time token and Telegram deeplink for the QR image.</p>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<strong>Poll QR confirmation</strong>
|
||||
<code>GET /userauth/qr/poll?token=...</code>
|
||||
<p>Returns pending, confirmed, or expired. On confirmed, also returns the user session.</p>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<strong>Read current session</strong>
|
||||
<code>GET /userauth/session</code>
|
||||
<p>Cookie-based session lookup used for initial auth check and fallback polling.</p>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<strong>Sync cart after login</strong>
|
||||
<code>POST /usersession/{sessionId}</code>
|
||||
<p>Existing cart payload is preserved. Only the namespace is generalized for reuse.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metadata">
|
||||
<strong>Behavior kept intact</strong>
|
||||
<ul>
|
||||
<li>Open Telegram directly from the primary button.</li>
|
||||
<li>Show QR immediately and poll every 3 seconds.</li>
|
||||
<li>Expire the QR after 100 checks and allow manual refresh.</li>
|
||||
<li>Re-check cookie session if QR creation fails.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="login-overlay">
|
||||
<div class="login-dialog">
|
||||
<button class="close-btn" type="button" aria-label="Close dialog">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="dialog-content" [attr.data-state]="state()" id="dialog-content">
|
||||
<div class="login-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2>Login required</h2>
|
||||
<p class="login-desc">Please log in via Telegram to proceed with your order.</p>
|
||||
|
||||
<div class="login-status checking">
|
||||
<div class="spinner"></div>
|
||||
<span>Checking...</span>
|
||||
</div>
|
||||
|
||||
<div class="action-block">
|
||||
<button class="telegram-btn" type="button">
|
||||
<svg class="tg-icon" width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"></path>
|
||||
</svg>
|
||||
Log in with Telegram
|
||||
</button>
|
||||
|
||||
<div class="qr-section">
|
||||
<p class="qr-hint">Or scan the QR code</p>
|
||||
|
||||
<div class="qr-container qr-loading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
|
||||
<div class="qr-container qr-ready">
|
||||
<img
|
||||
src="https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=https%3A%2F%2Ft.me%2Fuserauth_bot%3Fstart%3Dlogin_sample_token"
|
||||
alt="QR Code"
|
||||
width="180"
|
||||
height="180"
|
||||
loading="eager"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="qr-container qr-expired" role="button" tabindex="0" aria-label="Refresh QR code">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 4v6h6M23 20v-6h-6"></path>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
|
||||
</svg>
|
||||
<span>QR code expired. Click to refresh</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="login-note">You will be redirected back after login.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<telegram-userauth></telegram-userauth>
|
||||
|
||||
411
src/app/app.scss
411
src/app/app.scss
@@ -1,415 +1,4 @@
|
||||
:host {
|
||||
--bg-page: linear-gradient(135deg, #f4f7fb 0%, #e8eef4 100%);
|
||||
--bg-card: #ffffff;
|
||||
--bg-hover: #f0f0f0;
|
||||
--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: grid;
|
||||
grid-template-columns: minmax(320px, 448px) minmax(320px, 560px);
|
||||
gap: 32px;
|
||||
padding: 40px 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 28px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 18px 50px rgba(38, 52, 73, 0.12);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.info h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 32px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.info p {
|
||||
margin: 0 0 18px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.state-switcher {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 20px 0 24px;
|
||||
}
|
||||
|
||||
.state-switcher button {
|
||||
border: 1px solid #cfd8e3;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: var(--text-primary);
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.state-switcher button.active {
|
||||
border-color: var(--accent-color);
|
||||
background: var(--accent-light);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.api-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.api-card {
|
||||
background: #fff;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 16px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.api-card strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.api-card code {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f3f7fb;
|
||||
color: #21425f;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.api-card p {
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-overlay {
|
||||
position: relative;
|
||||
min-height: 700px;
|
||||
border-radius: 28px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.2s ease;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.login-dialog {
|
||||
position: relative;
|
||||
background: var(--bg-card);
|
||||
border-radius: 20px;
|
||||
padding: 32px 28px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow);
|
||||
animation: scaleIn 0.25s ease;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.login-dialog h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.login-desc {
|
||||
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:hover {
|
||||
background: var(--telegram-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3);
|
||||
}
|
||||
|
||||
.telegram-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.tg-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.qr-hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.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: 204px;
|
||||
height: 204px;
|
||||
}
|
||||
|
||||
.qr-loading .spinner,
|
||||
.login-status .spinner {
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.qr-loading .spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #e0e0e0;
|
||||
border-top-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.qr-expired {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 204px;
|
||||
height: 204px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.qr-expired:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.qr-expired span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-note {
|
||||
margin: 16px 0 0;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.login-status {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-status .spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-top-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.dialog-content[data-state='checking'] .login-status {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dialog-content[data-state='checking'] .action-block,
|
||||
.dialog-content[data-state='loading'] .qr-ready,
|
||||
.dialog-content[data-state='loading'] .qr-expired,
|
||||
.dialog-content[data-state='expired'] .qr-ready,
|
||||
.dialog-content[data-state='expired'] .qr-loading,
|
||||
.dialog-content[data-state='error'] .qr-loading,
|
||||
.dialog-content[data-state='error'] .qr-expired,
|
||||
.dialog-content[data-state='checking'] .qr-section,
|
||||
.dialog-content[data-state='checking'] .login-note {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dialog-content[data-state='ready'] .qr-loading,
|
||||
.dialog-content[data-state='ready'] .qr-expired,
|
||||
.dialog-content[data-state='ready'] .qr-error,
|
||||
.dialog-content[data-state='loading'] .qr-ready,
|
||||
.dialog-content[data-state='loading'] .qr-expired,
|
||||
.dialog-content[data-state='loading'] .qr-error,
|
||||
.dialog-content[data-state='expired'] .qr-loading,
|
||||
.dialog-content[data-state='expired'] .qr-ready,
|
||||
.dialog-content[data-state='expired'] .qr-error,
|
||||
.dialog-content[data-state='error'] .qr-loading,
|
||||
.dialog-content[data-state='error'] .qr-expired {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dialog-content[data-state='error'] .qr-ready {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
margin-top: 22px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid #e9edf2;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.metadata ul {
|
||||
margin: 10px 0 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.metadata li + li {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.page {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 24px 16px 32px;
|
||||
}
|
||||
|
||||
.login-overlay {
|
||||
min-height: 560px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.panel {
|
||||
border-radius: 22px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.login-dialog {
|
||||
padding: 24px 20px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.qr-container img {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.qr-loading,
|
||||
.qr-expired {
|
||||
width: 164px;
|
||||
height: 164px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';
|
||||
import { defineTelegramUserAuthElement } from '../package';
|
||||
|
||||
type DialogState = 'ready' | 'loading' | 'checking' | 'expired' | 'error';
|
||||
defineTelegramUserAuthElement();
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.scss'
|
||||
styleUrl: './app.scss',
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class App {
|
||||
protected readonly state = signal<DialogState>('ready');
|
||||
protected readonly states: DialogState[] = ['ready', 'loading', 'checking', 'expired', 'error'];
|
||||
|
||||
protected setState(state: DialogState): void {
|
||||
this.state.set(state);
|
||||
}
|
||||
}
|
||||
export class App {}
|
||||
|
||||
740
src/package/index.ts
Normal file
740
src/package/index.ts
Normal file
@@ -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<TelegramUserAuthConfig> = {};
|
||||
|
||||
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<TelegramUserAuthConfig>) {
|
||||
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<void> {
|
||||
this.started = true;
|
||||
await this.initializeAuth();
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
await this.startQrFlow();
|
||||
}
|
||||
|
||||
private applyConfig(): void {
|
||||
this.config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...this.readAttributeConfig(),
|
||||
...this.optionOverrides
|
||||
};
|
||||
}
|
||||
|
||||
private readAttributeConfig(): Partial<TelegramUserAuthConfig> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<QrCreateResponse>(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<void> {
|
||||
this.pollAttempts += 1;
|
||||
|
||||
if (this.pollAttempts >= this.config.maxPollAttempts) {
|
||||
this.stopPolling();
|
||||
this.setState('expired');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState('checking');
|
||||
|
||||
try {
|
||||
const response = await this.fetchJson<QrPollResponse>(
|
||||
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<void> {
|
||||
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<TelegramUserAuthSession> {
|
||||
return this.fetchJson<TelegramUserAuthSession>(this.buildApiUrl('/userauth/session'), {
|
||||
credentials: 'include'
|
||||
});
|
||||
}
|
||||
|
||||
private handleAuthenticated(session: TelegramUserAuthSession): void {
|
||||
this.stopPolling();
|
||||
this.session = session;
|
||||
this.setState('authenticated');
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<TelegramUserAuthAuthenticatedDetail>('userauth-authenticated', {
|
||||
detail: { session },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private setState(state: AuthState): void {
|
||||
this.state = state;
|
||||
this.render();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<TelegramUserAuthStateChangeDetail>('userauth-statechange', {
|
||||
detail: { state },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private dispatchPackageError(message: string): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<TelegramUserAuthErrorDetail>('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<void> {
|
||||
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<T>(input: RequestInfo | URL, init: RequestInit): Promise<T> {
|
||||
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
|
||||
? `
|
||||
<h1>Authenticated</h1>
|
||||
<p class="status-copy">${this.escapeHtml(this.getStatusMessage())}</p>
|
||||
<div class="session-card">
|
||||
<strong>${this.escapeHtml(this.session.displayName)}</strong>
|
||||
<span>${this.escapeHtml(this.session.username ? `@${this.session.username}` : `Telegram user ${this.session.telegramUserId}`)}</span>
|
||||
<span>${this.escapeHtml(`Session ${this.session.sessionId}`)}</span>
|
||||
<span>${this.escapeHtml(`Expires ${new Date(this.session.expiresAt).toLocaleString()}`)}</span>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<h1>${this.escapeHtml(this.config.title)}</h1>
|
||||
<p class="status-copy">${this.escapeHtml(this.getStatusMessage())}</p>
|
||||
<button class="telegram-btn" type="button" data-action="open" ${!this.qrUrl && this.state === 'loading' ? 'disabled' : ''}>
|
||||
<svg class="tg-icon" width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"></path>
|
||||
</svg>
|
||||
Open Telegram
|
||||
</button>
|
||||
<div class="qr-section">${this.renderQrSection()}</div>
|
||||
<button class="secondary-btn" type="button" data-action="refresh">Generate new QR</button>
|
||||
`;
|
||||
|
||||
this.root.innerHTML = `
|
||||
<style>${COMPONENT_STYLES}</style>
|
||||
<main class="page">
|
||||
<section class="login-card">
|
||||
<div class="login-icon" aria-hidden="true">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
${sessionContent}
|
||||
</section>
|
||||
</main>
|
||||
`;
|
||||
|
||||
this.root.querySelector<HTMLButtonElement>('[data-action="open"]')?.addEventListener('click', () => {
|
||||
void this.openTelegram();
|
||||
});
|
||||
|
||||
this.root.querySelector<HTMLButtonElement>('[data-action="refresh"]')?.addEventListener('click', () => {
|
||||
void this.startQrFlow();
|
||||
});
|
||||
}
|
||||
|
||||
private renderQrSection(): string {
|
||||
if (this.state === 'loading') {
|
||||
return '<div class="qr-container qr-loading" aria-live="polite"><div class="spinner"></div></div>';
|
||||
}
|
||||
|
||||
if (this.state === 'expired' || this.state === 'error') {
|
||||
return `
|
||||
<button class="qr-container qr-expired" type="button" data-action="refresh" aria-label="Refresh QR code">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M1 4v6h6M23 20v-6h-6"></path>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
|
||||
</svg>
|
||||
<span>Refresh QR code</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.qrImageUrl) {
|
||||
return `<div class="qr-container qr-ready"><img src="${this.escapeAttribute(this.qrImageUrl)}" alt="Telegram login QR code" width="${this.config.qrSize}" height="${this.config.qrSize}" loading="eager" /></div>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user