Tom716 commited on
Commit
dcd3d60
·
verified ·
1 Parent(s): 04cb3a8

Upload 23 files

Browse files
projects/telegram-send-demo/.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .env.local
5
+ *.log
6
+ .DS_Store
7
+ .vite/
8
+ .playwright-mcp/
9
+ *.tsbuildinfo
projects/telegram-send-demo/DOCS/plan.md ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project Plan: NestJS + grammY + React Telegram Demo App
2
+
3
+ ## 1. Objective
4
+ Build a lightweight application to dispatch Telegram notifications to a specific Chat ID, featuring interactive Inline Keyboards and a web frontend for testing.
5
+
6
+ ---
7
+
8
+ ## 2. System Architecture
9
+
10
+ ```text
11
+ ┌─────────────────┐ HTTP POST ┌─────────────────────┐
12
+ │ React Frontend ├──────────────────────►│ NestJS Backend │
13
+ │ (Vite App) │ │ (Telegram Service) │
14
+ └─────────────────┘ └──────────┬──────────┘
15
+ │
16
+ Telegram Bot API (grammY)
17
+ │
18
+ ▼
19
+ ┌─────────────────────┐
20
+ │ User Telegram App │
21
+ │ [ Interactive UI ] │
22
+ └─────────────────────┘
23
+
24
+ ```
25
+ ---
26
+
27
+ ## 3. project structure
28
+
29
+ ```
30
+ telegram-demo/
31
+ ├── backend/ # NestJS API
32
+ │ ├── src/
33
+ │ │ ├── telegram/
34
+ │ │ │ ├── telegram.controller.ts
35
+ │ │ │ ├── telegram.service.ts
36
+ │ │ │ └── telegram.module.ts
37
+ │ │ ├── app.module.ts
38
+ │ │ └── main.ts
39
+ │ ├── package.json
40
+ │ └── tsconfig.json
41
+ │
42
+ └── frontend/ # React (Vite) Testing UI
43
+ ├── src/
44
+ │ ├── App.jsx
45
+ │ ├── App.css
46
+ │ └── main.jsx
47
+ ├── package.json
48
+ └── vite.config.js
49
+ ```
projects/telegram-send-demo/README.md ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Telegram Notifier Demo
2
+
3
+ NestJS + grammY backend with a React (Vite) test UI. Send a notification to a
4
+ Telegram chat and act on it with inline **Mark Done ✅** / **Snooze ⏰** buttons.
5
+
6
+ ```
7
+ backend/ NestJS API (port 3000)
8
+ src/telegram/ TelegramModule, service, controller
9
+ frontend/ React + Vite UI (port 5173)
10
+ ```
11
+
12
+ ## 1. Create a bot
13
+
14
+ 1. Message [@BotFather](https://t.me/BotFather) on Telegram, send `/newbot`, and copy the token.
15
+ 2. In `backend/`, copy `.env.example` to `.env` and set `TELEGRAM_BOT_TOKEN`.
16
+ 3. Open your new bot and tap **Start** so it is allowed to message you.
17
+
18
+ ## 2. Run the backend
19
+
20
+ ```bash
21
+ cd backend
22
+ npm install
23
+ npm run start:dev
24
+ ```
25
+
26
+ The bot starts long polling on boot. Verify the API:
27
+
28
+ ```bash
29
+ curl http://localhost:3000/api/telegram/health
30
+ # {"status":"ok"}
31
+ ```
32
+
33
+ Open the interactive API documentation at http://localhost:3000/api/docs.
34
+
35
+ ## 3. Run the frontend
36
+
37
+ ```bash
38
+ cd frontend
39
+ npm install
40
+ npm run dev
41
+ ```
42
+
43
+ Open http://localhost:5173 and click **Connect Telegram**. Telegram opens, you
44
+ press **Start** once, and the app fills in your Chat ID automatically. Type a
45
+ message and send it.
46
+
47
+ ## How a zero-knowledge user connects
48
+
49
+ Nobody needs to know what a Chat ID is:
50
+
51
+ 1. The UI's **Connect Telegram** button asks the backend for a one-time code.
52
+ 2. The backend creates a one-time code and returns two links:
53
+ - `deepLink`: `https://t.me/nemo_calls_bot?start=link_4dwNOSW4syjy` (mobile / desktop app)
54
+ - `webLink`: `https://web.telegram.org/z/#?tgaddr=tg://resolve?domain=nemo_calls_bot%26start=link_4dwNOSW4syjy` (Telegram Web)
55
+ 3. The user presses **Start** in Telegram. The bot receives `/start link_…`,
56
+ binds that chat to the code, and replies "✅ Connected!".
57
+ 4. The UI is polling `GET /api/telegram/link/:code`; it sees `connected`, stores
58
+ the Chat ID in `localStorage`, and shows the green connected card.
59
+
60
+ The Chat ID is still visible under **Use a Chat ID manually** for debugging, but
61
+ it is an implementation detail rather than something users must copy.
62
+
63
+ Link codes are single-use, expire after 10 minutes, and are kept in memory, so
64
+ restarting the backend invalidates pending links.
65
+
66
+ ## API
67
+
68
+ `POST /api/telegram/send`
69
+
70
+ ```json
71
+ { "chatId": "123456789", "message": "Test notification" }
72
+ ```
73
+
74
+ Success: `200 { "ok": true, "messageId": 42 }`. Validation errors return `400`,
75
+ Telegram delivery failures return `502`.
76
+
77
+ `GET /api/telegram/health` → `{ "status": "ok" }`
78
+
79
+ `GET /api/telegram/bot` → `{ "username": "nemo_calls_bot", "deepLink": "https://t.me/nemo_calls_bot" }`
80
+
81
+ `POST /api/telegram/link` → `{ "code": "…", "username": "…", "deepLink": "https://t.me/…?start=link_…", "webLink": "https://web.telegram.org/z/#?tgaddr=…", "expiresAt": "…" }`
82
+
83
+ `GET /api/telegram/link/:code` → `{ "status": "pending" }`, `{ "status": "connected", "chatId": "…" }`, or `{ "status": "expired" }`
84
+
85
+ ## Deploying behind Caddy (Hugging Face Spaces)
86
+
87
+ The `Caddyfile` + `supervisord.conf` setup (Caddy on `:7860` → NestJS on `:3000`)
88
+ is sound: if `GET /api/telegram/health` returns `{"status":"ok"}` through the
89
+ public URL, the proxy is working.
90
+
91
+ A deployed copy still returns
92
+
93
+ ```json
94
+ { "message": "Telegram bot is not reachable yet: Network request for 'getMe' failed!", "statusCode": 503 }
95
+ ```
96
+
97
+ because **Hugging Face Spaces block outbound traffic to `api.telegram.org`** at
98
+ the network level. No code or Caddy change can fix that from inside the Space.
99
+ Calls have to go through a relay outside HF:
100
+
101
+ - The backend reads `TELEGRAM_API_ROOT` at boot (default
102
+ `https://api.telegram.org`) and sends every Bot API call through it.
103
+ - A working relay is the Cloudflare Pages bridge
104
+ `https://n8n-bridge-2gt.pages.dev`, which forwards
105
+ `https://n8n-bridge-2gt.pages.dev/bot<token>/<method>` to
106
+ `https://api.telegram.org/bot<token>/<method>` unchanged (GET, POST, query
107
+ strings, and long polling included).
108
+
109
+ In the Space open **Settings → Variables and secrets** and add:
110
+
111
+ ```
112
+ TELEGRAM_API_ROOT = https://n8n-bridge-2gt.pages.dev
113
+ ```
114
+
115
+ Then **Restart** the Space. Logs should show:
116
+
117
+ ```
118
+ [TelegramService] Using Telegram Bot API root https://n8n-bridge-2gt.pages.dev
119
+ [TelegramService] Telegram bot @your_bot is polling
120
+ ```
121
+
122
+ If the second line is missing, the Space cannot reach the Bot API (or the
123
+ relay). Requests still go out over HTTPS with the token in the path, so keep the
124
+ relay private if you build your own; the alternative is to move the backend to a
125
+ host without egress filtering (Render, Fly.io, Railway, Koyeb, a VPS) and keep
126
+ only the frontend on HF. Point the frontend at that API with `VITE_API_URL` and
127
+ add its origin to `CORS_ORIGINS`.
128
+
129
+ ## Notes
130
+
131
+ - The backend fails fast at boot if `TELEGRAM_BOT_TOKEN` is missing, because the
132
+ bot cannot poll without it.
133
+ - Only one polling process per bot token can run at a time. Stop other instances
134
+ (or a deployed copy) if you see `409 Conflict`.
135
+ - The frontend talks to `http://localhost:3000` by default; override with
136
+ `VITE_API_URL` when running elsewhere.
137
+ - The backend allows `http://localhost:5173` by default. Set `CORS_ORIGINS` in
138
+ `backend/.env` to your Cloudflare Pages origin, for example
139
+ `CORS_ORIGINS=https://your-app.pages.dev`. Multiple origins can be separated
140
+ with commas.
141
+ - This is still a demo: links and chat bindings live in memory, there are no user
142
+ accounts, and anyone holding a valid link code can read the bound Chat ID. For
143
+ production, persist links and chat bindings in a database, associate them with
144
+ an authenticated user, and rate-limit `POST /api/telegram/link`.
projects/telegram-send-demo/backend/.env.example ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Get this from @BotFather on Telegram
2
+ TELEGRAM_BOT_TOKEN=123456789:AAYourBotTokenHere
3
+
4
+ # Optional, defaults to 3000
5
+ PORT=3000
6
+
7
+ # Optional. Root URL of the Telegram Bot API. Defaults to the official server.
8
+ # Hosts that block api.telegram.org (Hugging Face Spaces does) must point this
9
+ # at a relay, e.g. an existing Cloudflare Pages bridge:
10
+ # TELEGRAM_API_ROOT=https://n8n-bridge-2gt.pages.dev
11
+
12
+ # Optional, comma-separated frontend origins. Defaults to local Vite dev server.
13
+ # Example: CORS_ORIGINS=https://your-app.pages.dev
14
+ CORS_ORIGINS=http://localhost:5173
projects/telegram-send-demo/backend/nest-cli.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true
7
+ }
8
+ }
projects/telegram-send-demo/backend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
projects/telegram-send-demo/backend/package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "telegram-demo-backend",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "NestJS + grammY Telegram notification demo API",
6
+ "scripts": {
7
+ "build": "nest build",
8
+ "start": "nest start",
9
+ "start:dev": "nest start --watch",
10
+ "start:prod": "node dist/main.js"
11
+ },
12
+ "dependencies": {
13
+ "@nestjs/common": "^11.0.1",
14
+ "@nestjs/config": "^4.0.0",
15
+ "@nestjs/core": "^11.0.1",
16
+ "@nestjs/platform-express": "^11.0.1",
17
+ "@nestjs/swagger": "^11.4.7",
18
+ "grammy": "^1.38.0",
19
+ "reflect-metadata": "^0.2.2",
20
+ "rxjs": "^7.8.1"
21
+ },
22
+ "devDependencies": {
23
+ "@nestjs/cli": "^11.0.0",
24
+ "@nestjs/schematics": "^11.0.0",
25
+ "@types/express": "^5.0.0",
26
+ "@types/node": "^22.10.7",
27
+ "ts-loader": "^9.5.2",
28
+ "ts-node": "^10.9.2",
29
+ "tsconfig-paths": "^4.2.0",
30
+ "typescript": "^5.7.3"
31
+ }
32
+ }
projects/telegram-send-demo/backend/src/app.module.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from '@nestjs/common';
2
+ import { ConfigModule } from '@nestjs/config';
3
+ import { TelegramModule } from './telegram/telegram.module';
4
+
5
+ @Module({
6
+ imports: [
7
+ ConfigModule.forRoot({
8
+ isGlobal: true,
9
+ ignoreEnvFile: false,
10
+ }),
11
+ TelegramModule,
12
+ ],
13
+ })
14
+ export class AppModule {}
projects/telegram-send-demo/backend/src/main.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as dns from 'node:dns';
2
+ import { Logger } from '@nestjs/common';
3
+ import { NestFactory } from '@nestjs/core';
4
+ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
5
+ import { AppModule } from './app.module';
6
+
7
+ dns.setDefaultResultOrder('ipv4first');
8
+
9
+ async function bootstrap(): Promise<void> {
10
+ const app = await NestFactory.create(AppModule);
11
+
12
+ const swaggerConfig = new DocumentBuilder()
13
+ .setTitle('Telegram Notifier API')
14
+ .setDescription('API for sending Telegram notifications and connecting chats.')
15
+ .setVersion('1.0')
16
+ .build();
17
+ const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
18
+ SwaggerModule.setup('api/docs', app, swaggerDocument);
19
+
20
+ const corsOrigins = (process.env.CORS_ORIGINS ?? 'http://localhost:5173')
21
+ .split(',')
22
+ .map((origin) => origin.trim())
23
+ .filter(Boolean);
24
+
25
+ app.enableCors({
26
+ origin: corsOrigins,
27
+ methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
28
+ credentials: true,
29
+ });
30
+ app.enableShutdownHooks();
31
+
32
+ const port = Number(process.env.PORT ?? 3000);
33
+ await app.listen(port);
34
+
35
+ Logger.log(`API listening on http://localhost:${port}`, 'Bootstrap');
36
+ }
37
+
38
+ void bootstrap();
projects/telegram-send-demo/backend/src/telegram/telegram.controller.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ BadGatewayException,
3
+ BadRequestException,
4
+ Body,
5
+ Controller,
6
+ Get,
7
+ HttpCode,
8
+ HttpStatus,
9
+ Param,
10
+ Post,
11
+ ServiceUnavailableException,
12
+ } from '@nestjs/common';
13
+ import { TelegramService } from './telegram.service';
14
+
15
+ interface SendNotificationBody {
16
+ chatId?: string | number;
17
+ message?: string;
18
+ }
19
+
20
+ const MAX_MESSAGE_LENGTH = 4096; // Telegram's per-message limit
21
+
22
+ @Controller('api/telegram')
23
+ export class TelegramController {
24
+ constructor(private readonly telegramService: TelegramService) {}
25
+
26
+ @Post('send')
27
+ @HttpCode(HttpStatus.OK)
28
+ async send(@Body() body: SendNotificationBody) {
29
+ const chatId = body?.chatId === undefined || body?.chatId === null ? '' : String(body.chatId).trim();
30
+ const message = typeof body?.message === 'string' ? body.message.trim() : '';
31
+
32
+ if (!/^-?\d+$/.test(chatId)) {
33
+ throw new BadRequestException('"chatId" must be a numeric Telegram chat ID, e.g. 123456789.');
34
+ }
35
+
36
+ if (!message) {
37
+ throw new BadRequestException('"message" is required.');
38
+ }
39
+
40
+ if (message.length > MAX_MESSAGE_LENGTH) {
41
+ throw new BadRequestException(
42
+ `"message" must be ${MAX_MESSAGE_LENGTH} characters or fewer.`,
43
+ );
44
+ }
45
+
46
+ try {
47
+ const messageId = await this.telegramService.sendNotification(chatId, message);
48
+ return { ok: true, messageId };
49
+ } catch (error) {
50
+ const reason = error instanceof Error ? error.message : 'Unknown Telegram API error';
51
+ throw new BadGatewayException(`Telegram could not deliver the message: ${reason}`);
52
+ }
53
+ }
54
+
55
+ @Get('health')
56
+ health(): { status: 'ok' } {
57
+ return { status: 'ok' };
58
+ }
59
+
60
+ /** Bot username and public t.me link, used by the "Connect Telegram" button. */
61
+ @Get('bot')
62
+ async bot(): Promise<{ username: string; deepLink: string }> {
63
+ try {
64
+ return await this.telegramService.getBotInfo();
65
+ } catch (error) {
66
+ const reason = error instanceof Error ? error.message : 'Unknown Telegram API error';
67
+ throw new ServiceUnavailableException(`Telegram bot is not reachable yet: ${reason}`);
68
+ }
69
+ }
70
+
71
+ /** Starts a one-time linking handshake. */
72
+ @Post('link')
73
+ @HttpCode(HttpStatus.OK)
74
+ async createLink() {
75
+ try {
76
+ return await this.telegramService.createConnectLink();
77
+ } catch (error) {
78
+ const reason = error instanceof Error ? error.message : 'Unknown Telegram API error';
79
+ throw new ServiceUnavailableException(`Could not create a connect link: ${reason}`);
80
+ }
81
+ }
82
+
83
+ /** Polled by the UI until the user presses Start in Telegram. */
84
+ @Get('link/:code')
85
+ linkStatus(@Param('code') code: string) {
86
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(code)) {
87
+ throw new BadRequestException('Invalid connect code.');
88
+ }
89
+ return this.telegramService.getLinkStatus(code);
90
+ }
91
+ }
projects/telegram-send-demo/backend/src/telegram/telegram.module.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from '@nestjs/common';
2
+ import { TelegramController } from './telegram.controller';
3
+ import { TelegramService } from './telegram.service';
4
+
5
+ @Module({
6
+ controllers: [TelegramController],
7
+ providers: [TelegramService],
8
+ exports: [TelegramService],
9
+ })
10
+ export class TelegramModule {}
projects/telegram-send-demo/backend/src/telegram/telegram.service.ts ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { randomBytes } from 'node:crypto';
2
+ import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
3
+ import { ConfigService } from '@nestjs/config';
4
+ import { Bot, Context, InlineKeyboard } from 'grammy';
5
+
6
+ /** The callback_data values used by the inline keyboard below. */
7
+ type ActionData = 'action_done' | 'action_snooze';
8
+
9
+ export type LinkStatus =
10
+ | { status: 'pending' }
11
+ | { status: 'connected'; chatId: string }
12
+ | { status: 'expired' };
13
+
14
+ export interface ConnectLink {
15
+ code: string;
16
+ username: string;
17
+ deepLink: string;
18
+ webLink: string;
19
+ expiresAt: string;
20
+ }
21
+
22
+ interface PendingLink {
23
+ expiresAt: number;
24
+ chatId?: string;
25
+ }
26
+
27
+ const ACTION_LABELS: Record<ActionData, string> = {
28
+ action_done: 'Marked as done ✅',
29
+ action_snooze: 'Snoozed ⏰',
30
+ };
31
+
32
+ const LINK_TTL_MS = 10 * 60 * 1000; // 10 minutes
33
+ const LINK_PAYLOAD_PREFIX = 'link_';
34
+ const DEFAULT_API_ROOT = 'https://api.telegram.org';
35
+ /** Long-polling timeout. Kept short so relays and proxies can hold the request. */
36
+ const POLLING_TIMEOUT_SECONDS = 10;
37
+
38
+ function isActionData(data: string): data is ActionData {
39
+ return data === 'action_done' || data === 'action_snooze';
40
+ }
41
+
42
+ /** Inline keyboard attached to every notification. */
43
+ function notificationKeyboard(): InlineKeyboard {
44
+ return new InlineKeyboard()
45
+ .text('Mark Done ✅', 'action_done')
46
+ .text('Snooze ⏰', 'action_snooze');
47
+ }
48
+
49
+ @Injectable()
50
+ export class TelegramService implements OnModuleInit, OnModuleDestroy {
51
+ private readonly logger = new Logger(TelegramService.name);
52
+ private readonly token: string;
53
+ private readonly apiRoot: string;
54
+ private readonly pendingLinks = new Map<string, PendingLink>();
55
+ private bot?: Bot;
56
+ private botUsername?: string;
57
+
58
+ constructor(private readonly configService: ConfigService) {
59
+ this.token = this.configService.get<string>('TELEGRAM_BOT_TOKEN')?.trim() ?? '';
60
+ // Hosts such as Hugging Face Spaces block api.telegram.org; set
61
+ // TELEGRAM_API_ROOT to a relay that forwards to the Bot API to fix that.
62
+ this.apiRoot =
63
+ this.configService.get<string>('TELEGRAM_API_ROOT')?.trim().replace(/\/+$/, '') || DEFAULT_API_ROOT;
64
+ }
65
+
66
+ onModuleInit(): void {
67
+ if (!this.token) {
68
+ throw new Error(
69
+ 'TELEGRAM_BOT_TOKEN is missing. Copy backend/.env.example to backend/.env and set the token from @BotFather.',
70
+ );
71
+ }
72
+
73
+ this.bot = new Bot(this.token, {
74
+ client: {
75
+ apiRoot: this.apiRoot,
76
+ // Fail fast (30s) instead of hanging for minutes on a blocked network.
77
+ timeoutSeconds: 30,
78
+ },
79
+ });
80
+ this.registerHandlers();
81
+ this.logger.log(`Using Telegram Bot API root ${this.apiRoot}`);
82
+
83
+ // Never leave an unhandled rejection from the polling loop.
84
+ this.bot.catch((error) => {
85
+ this.logger.error(`Telegram bot error: ${error.message}`, error.stack);
86
+ });
87
+
88
+ // bot.start() resolves only when polling stops, so it must not be awaited here.
89
+ void this.bot
90
+ .start({
91
+ timeout: POLLING_TIMEOUT_SECONDS,
92
+ onStart: (me) => {
93
+ this.botUsername = me.username;
94
+ this.logger.log(`Telegram bot @${me.username} is polling`);
95
+ },
96
+ })
97
+ .catch((error: Error) => {
98
+ this.logger.error(`Telegram bot failed to start: ${error.message}`);
99
+ this.logStartupHint(error);
100
+ });
101
+ }
102
+
103
+ /**
104
+ * The most common deployment failure is not a bug: some hosts (notably
105
+ * Hugging Face Spaces) block outbound traffic to api.telegram.org, which
106
+ * surfaces as "Network request for 'getMe' failed!". Say so in the logs.
107
+ */
108
+ private logStartupHint(error: Error): void {
109
+ if (!/Network request/i.test(error.message)) return;
110
+
111
+ this.logger.error(
112
+ `Cannot reach the Telegram Bot API at ${this.apiRoot}. ` +
113
+ 'If this host blocks api.telegram.org (Hugging Face Spaces does), deploy a relay and set ' +
114
+ 'TELEGRAM_API_ROOT to it, or move the backend to a host with unrestricted outbound HTTPS.',
115
+ );
116
+ }
117
+
118
+ async onModuleDestroy(): Promise<void> {
119
+ if (!this.bot) return;
120
+
121
+ try {
122
+ await this.bot.stop();
123
+ this.logger.log('Telegram bot stopped');
124
+ } catch (error) {
125
+ this.logger.error(`Telegram bot failed to stop cleanly: ${(error as Error).message}`);
126
+ }
127
+ }
128
+
129
+ /** Sends `text` to `chatId` with the Mark Done / Snooze inline keyboard. */
130
+ async sendNotification(chatId: string, text: string): Promise<number> {
131
+ const bot = this.requireBot();
132
+
133
+ const message = await bot.api.sendMessage(chatId, text, {
134
+ reply_markup: notificationKeyboard(),
135
+ });
136
+
137
+ this.logger.log(`Notification sent to chat ${chatId} (message ${message.message_id})`);
138
+ return message.message_id;
139
+ }
140
+
141
+ /** Bot identity, used by the UI to label and link to the bot. */
142
+ async getBotInfo(): Promise<{ username: string; deepLink: string }> {
143
+ const username = await this.resolveBotUsername();
144
+ return { username, deepLink: `https://t.me/${username}` };
145
+ }
146
+
147
+ /**
148
+ * Creates a single-use code the user can redeem by opening a t.me deep link.
149
+ * In-memory only: restarting the backend invalidates outstanding links.
150
+ */
151
+ async createConnectLink(): Promise<ConnectLink> {
152
+ const username = await this.resolveBotUsername();
153
+ this.pruneLinks();
154
+
155
+ const code = randomBytes(9).toString('base64url');
156
+ const expiresAt = Date.now() + LINK_TTL_MS;
157
+ this.pendingLinks.set(code, { expiresAt });
158
+
159
+ const tgLink = `tg://resolve?domain=${username}&start=${LINK_PAYLOAD_PREFIX}${code}`;
160
+
161
+ return {
162
+ code,
163
+ username,
164
+ deepLink: `https://t.me/${username}?start=${LINK_PAYLOAD_PREFIX}${code}`,
165
+ webLink: `https://web.telegram.org/z/#?tgaddr=${encodeURIComponent(tgLink)}`,
166
+ expiresAt: new Date(expiresAt).toISOString(),
167
+ };
168
+ }
169
+
170
+ /** Polled by the frontend while the user is pressing Start in Telegram. */
171
+ getLinkStatus(code: string): LinkStatus {
172
+ const link = this.pendingLinks.get(code);
173
+
174
+ if (!link) return { status: 'expired' };
175
+
176
+ if (link.expiresAt <= Date.now()) {
177
+ this.pendingLinks.delete(code);
178
+ return { status: 'expired' };
179
+ }
180
+
181
+ return link.chatId ? { status: 'connected', chatId: link.chatId } : { status: 'pending' };
182
+ }
183
+
184
+ private async resolveBotUsername(): Promise<string> {
185
+ if (this.botUsername) return this.botUsername;
186
+
187
+ const me = await this.requireBot().api.getMe();
188
+ this.botUsername = me.username;
189
+ return me.username;
190
+ }
191
+
192
+ private completeLink(code: string, chatId: number): boolean {
193
+ const link = this.pendingLinks.get(code);
194
+
195
+ if (!link || link.expiresAt <= Date.now() || link.chatId) return false;
196
+
197
+ link.chatId = String(chatId);
198
+ this.logger.log(`Chat ${chatId} linked through a deep link`);
199
+ return true;
200
+ }
201
+
202
+ private pruneLinks(): void {
203
+ const now = Date.now();
204
+ for (const [code, link] of this.pendingLinks) {
205
+ if (link.expiresAt <= now) this.pendingLinks.delete(code);
206
+ }
207
+ }
208
+
209
+ private requireBot(): Bot {
210
+ if (!this.bot) {
211
+ throw new Error('Telegram bot is not initialised yet');
212
+ }
213
+ return this.bot;
214
+ }
215
+
216
+ private registerHandlers(): void {
217
+ const bot = this.requireBot();
218
+
219
+ bot.command('start', async (ctx) => {
220
+ const payload = ctx.match.trim();
221
+
222
+ if (payload.startsWith(LINK_PAYLOAD_PREFIX)) {
223
+ const code = payload.slice(LINK_PAYLOAD_PREFIX.length);
224
+ const connected = this.completeLink(code, ctx.chat.id);
225
+
226
+ await ctx.reply(
227
+ connected
228
+ ? '✅ Connected! Head back to the app and send your first notification.\n\nYou can close this chat.'
229
+ : 'This connect link is invalid, expired, or already used.\n\nGenerate a fresh one from the app.',
230
+ );
231
+ return;
232
+ }
233
+
234
+ await ctx.reply(
235
+ `Welcome! Your Chat ID is: ${ctx.chat.id}\n\n` +
236
+ 'Tip: the app\'s "Connect Telegram" button fills this in for you automatically.',
237
+ );
238
+ });
239
+
240
+ bot.on('callback_query:data', async (ctx) => {
241
+ const data = ctx.callbackQuery.data;
242
+
243
+ // Always acknowledge, otherwise the client spins for ~30s.
244
+ await ctx.answerCallbackQuery(
245
+ isActionData(data) ? { text: ACTION_LABELS[data] } : undefined,
246
+ );
247
+
248
+ if (!isActionData(data)) return;
249
+
250
+ await this.replaceNotificationStatus(ctx, ACTION_LABELS[data]);
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Rewrites the notification in place, dropping the inline keyboard so the
256
+ * action cannot be tapped twice.
257
+ */
258
+ private async replaceNotificationStatus(ctx: Context, status: string): Promise<void> {
259
+ const original = ctx.callbackQuery?.message?.text;
260
+
261
+ if (!original) {
262
+ this.logger.warn('Callback query has no editable text message; skipping edit');
263
+ return;
264
+ }
265
+
266
+ // Strip a status appended by an earlier tap before adding the new one.
267
+ const base = original.split('\n\n— ')[0];
268
+
269
+ try {
270
+ await ctx.editMessageText(`${base}\n\n— ${status}`, {
271
+ reply_markup: { inline_keyboard: [] },
272
+ });
273
+ } catch (error) {
274
+ // A repeated tap can hit "message is not modified"; that is harmless.
275
+ this.logger.debug(`Could not edit notification: ${(error as Error).message}`);
276
+ }
277
+ }
278
+ }
projects/telegram-send-demo/backend/tsconfig.build.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["node_modules", "dist", "test", "**/*.spec.ts"]
4
+ }
projects/telegram-send-demo/backend/tsconfig.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "declaration": true,
5
+ "removeComments": true,
6
+ "emitDecoratorMetadata": true,
7
+ "experimentalDecorators": true,
8
+ "allowSyntheticDefaultImports": true,
9
+ "esModuleInterop": true,
10
+ "target": "ES2022",
11
+ "sourceMap": true,
12
+ "outDir": "./dist",
13
+ "skipLibCheck": true,
14
+ "strict": true,
15
+ "strictNullChecks": true,
16
+ "noImplicitAny": true,
17
+ "strictBindCallApply": true,
18
+ "forceConsistentCasingInFileNames": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "rootDir": "./src"
21
+ },
22
+ "include": ["src/**/*"]
23
+ }
projects/telegram-send-demo/frontend/.env.example ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Set this for local development when the frontend runs separately from the API.
2
+ VITE_API_URL=http://localhost:3000
projects/telegram-send-demo/frontend/index.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <meta
8
+ name="description"
9
+ content="Send Telegram notifications with interactive Mark Done and Snooze buttons."
10
+ />
11
+ <title>Telegram Notifier</title>
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ <script type="module" src="/src/main.jsx"></script>
16
+ </body>
17
+ </html>
projects/telegram-send-demo/frontend/package-lock.json ADDED
@@ -0,0 +1,1839 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "telegram-demo-frontend",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "telegram-demo-frontend",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "react": "^19.1.0",
12
+ "react-dom": "^19.1.0"
13
+ },
14
+ "devDependencies": {
15
+ "@vitejs/plugin-react": "^4.4.1",
16
+ "vite": "^6.3.5"
17
+ }
18
+ },
19
+ "node_modules/@babel/code-frame": {
20
+ "version": "7.29.7",
21
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
22
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
23
+ "dev": true,
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "@babel/helper-validator-identifier": "^7.29.7",
27
+ "js-tokens": "^4.0.0",
28
+ "picocolors": "^1.1.1"
29
+ },
30
+ "engines": {
31
+ "node": ">=6.9.0"
32
+ }
33
+ },
34
+ "node_modules/@babel/compat-data": {
35
+ "version": "7.29.7",
36
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
37
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
38
+ "dev": true,
39
+ "license": "MIT",
40
+ "engines": {
41
+ "node": ">=6.9.0"
42
+ }
43
+ },
44
+ "node_modules/@babel/core": {
45
+ "version": "7.29.7",
46
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
47
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
48
+ "dev": true,
49
+ "license": "MIT",
50
+ "dependencies": {
51
+ "@babel/code-frame": "^7.29.7",
52
+ "@babel/generator": "^7.29.7",
53
+ "@babel/helper-compilation-targets": "^7.29.7",
54
+ "@babel/helper-module-transforms": "^7.29.7",
55
+ "@babel/helpers": "^7.29.7",
56
+ "@babel/parser": "^7.29.7",
57
+ "@babel/template": "^7.29.7",
58
+ "@babel/traverse": "^7.29.7",
59
+ "@babel/types": "^7.29.7",
60
+ "@jridgewell/remapping": "^2.3.5",
61
+ "convert-source-map": "^2.0.0",
62
+ "debug": "^4.1.0",
63
+ "gensync": "^1.0.0-beta.2",
64
+ "json5": "^2.2.3",
65
+ "semver": "^6.3.1"
66
+ },
67
+ "engines": {
68
+ "node": ">=6.9.0"
69
+ },
70
+ "funding": {
71
+ "type": "opencollective",
72
+ "url": "https://opencollective.com/babel"
73
+ }
74
+ },
75
+ "node_modules/@babel/generator": {
76
+ "version": "7.29.8",
77
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
78
+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
79
+ "dev": true,
80
+ "license": "MIT",
81
+ "dependencies": {
82
+ "@babel/parser": "^7.29.8",
83
+ "@babel/types": "^7.29.8",
84
+ "@jridgewell/gen-mapping": "^0.3.12",
85
+ "@jridgewell/trace-mapping": "^0.3.28",
86
+ "jsesc": "^3.0.2"
87
+ },
88
+ "engines": {
89
+ "node": ">=6.9.0"
90
+ }
91
+ },
92
+ "node_modules/@babel/helper-compilation-targets": {
93
+ "version": "7.29.7",
94
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
95
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
96
+ "dev": true,
97
+ "license": "MIT",
98
+ "dependencies": {
99
+ "@babel/compat-data": "^7.29.7",
100
+ "@babel/helper-validator-option": "^7.29.7",
101
+ "browserslist": "^4.24.0",
102
+ "lru-cache": "^5.1.1",
103
+ "semver": "^6.3.1"
104
+ },
105
+ "engines": {
106
+ "node": ">=6.9.0"
107
+ }
108
+ },
109
+ "node_modules/@babel/helper-globals": {
110
+ "version": "7.29.7",
111
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
112
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
113
+ "dev": true,
114
+ "license": "MIT",
115
+ "engines": {
116
+ "node": ">=6.9.0"
117
+ }
118
+ },
119
+ "node_modules/@babel/helper-module-imports": {
120
+ "version": "7.29.7",
121
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
122
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
123
+ "dev": true,
124
+ "license": "MIT",
125
+ "dependencies": {
126
+ "@babel/traverse": "^7.29.7",
127
+ "@babel/types": "^7.29.7"
128
+ },
129
+ "engines": {
130
+ "node": ">=6.9.0"
131
+ }
132
+ },
133
+ "node_modules/@babel/helper-module-transforms": {
134
+ "version": "7.29.7",
135
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
136
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
137
+ "dev": true,
138
+ "license": "MIT",
139
+ "dependencies": {
140
+ "@babel/helper-module-imports": "^7.29.7",
141
+ "@babel/helper-validator-identifier": "^7.29.7",
142
+ "@babel/traverse": "^7.29.7"
143
+ },
144
+ "engines": {
145
+ "node": ">=6.9.0"
146
+ },
147
+ "peerDependencies": {
148
+ "@babel/core": "^7.0.0"
149
+ }
150
+ },
151
+ "node_modules/@babel/helper-plugin-utils": {
152
+ "version": "7.29.7",
153
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
154
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
155
+ "dev": true,
156
+ "license": "MIT",
157
+ "engines": {
158
+ "node": ">=6.9.0"
159
+ }
160
+ },
161
+ "node_modules/@babel/helper-string-parser": {
162
+ "version": "7.29.11",
163
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz",
164
+ "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==",
165
+ "dev": true,
166
+ "license": "MIT",
167
+ "engines": {
168
+ "node": ">=6.9.0"
169
+ }
170
+ },
171
+ "node_modules/@babel/helper-validator-identifier": {
172
+ "version": "7.29.7",
173
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
174
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
175
+ "dev": true,
176
+ "license": "MIT",
177
+ "engines": {
178
+ "node": ">=6.9.0"
179
+ }
180
+ },
181
+ "node_modules/@babel/helper-validator-option": {
182
+ "version": "7.29.7",
183
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
184
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
185
+ "dev": true,
186
+ "license": "MIT",
187
+ "engines": {
188
+ "node": ">=6.9.0"
189
+ }
190
+ },
191
+ "node_modules/@babel/helpers": {
192
+ "version": "7.29.10",
193
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz",
194
+ "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==",
195
+ "dev": true,
196
+ "license": "MIT",
197
+ "dependencies": {
198
+ "@babel/template": "^7.29.7",
199
+ "@babel/types": "^7.29.8"
200
+ },
201
+ "engines": {
202
+ "node": ">=6.9.0"
203
+ }
204
+ },
205
+ "node_modules/@babel/parser": {
206
+ "version": "7.29.11",
207
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz",
208
+ "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==",
209
+ "dev": true,
210
+ "license": "MIT",
211
+ "dependencies": {
212
+ "@babel/types": "^7.29.11"
213
+ },
214
+ "bin": {
215
+ "parser": "bin/babel-parser.js"
216
+ },
217
+ "engines": {
218
+ "node": ">=6.0.0"
219
+ }
220
+ },
221
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
222
+ "version": "7.29.7",
223
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
224
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
225
+ "dev": true,
226
+ "license": "MIT",
227
+ "dependencies": {
228
+ "@babel/helper-plugin-utils": "^7.29.7"
229
+ },
230
+ "engines": {
231
+ "node": ">=6.9.0"
232
+ },
233
+ "peerDependencies": {
234
+ "@babel/core": "^7.0.0-0"
235
+ }
236
+ },
237
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
238
+ "version": "7.29.7",
239
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
240
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
241
+ "dev": true,
242
+ "license": "MIT",
243
+ "dependencies": {
244
+ "@babel/helper-plugin-utils": "^7.29.7"
245
+ },
246
+ "engines": {
247
+ "node": ">=6.9.0"
248
+ },
249
+ "peerDependencies": {
250
+ "@babel/core": "^7.0.0-0"
251
+ }
252
+ },
253
+ "node_modules/@babel/template": {
254
+ "version": "7.29.7",
255
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
256
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
257
+ "dev": true,
258
+ "license": "MIT",
259
+ "dependencies": {
260
+ "@babel/code-frame": "^7.29.7",
261
+ "@babel/parser": "^7.29.7",
262
+ "@babel/types": "^7.29.7"
263
+ },
264
+ "engines": {
265
+ "node": ">=6.9.0"
266
+ }
267
+ },
268
+ "node_modules/@babel/traverse": {
269
+ "version": "7.29.10",
270
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz",
271
+ "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==",
272
+ "dev": true,
273
+ "license": "MIT",
274
+ "dependencies": {
275
+ "@babel/code-frame": "^7.29.7",
276
+ "@babel/generator": "^7.29.8",
277
+ "@babel/helper-globals": "^7.29.7",
278
+ "@babel/parser": "^7.29.9",
279
+ "@babel/template": "^7.29.7",
280
+ "@babel/types": "^7.29.8",
281
+ "debug": "^4.3.1"
282
+ },
283
+ "engines": {
284
+ "node": ">=6.9.0"
285
+ }
286
+ },
287
+ "node_modules/@babel/types": {
288
+ "version": "7.29.11",
289
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz",
290
+ "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==",
291
+ "dev": true,
292
+ "license": "MIT",
293
+ "dependencies": {
294
+ "@babel/helper-string-parser": "^7.29.11",
295
+ "@babel/helper-validator-identifier": "^7.29.7"
296
+ },
297
+ "engines": {
298
+ "node": ">=6.9.0"
299
+ }
300
+ },
301
+ "node_modules/@esbuild/aix-ppc64": {
302
+ "version": "0.25.12",
303
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
304
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
305
+ "cpu": [
306
+ "ppc64"
307
+ ],
308
+ "dev": true,
309
+ "license": "MIT",
310
+ "optional": true,
311
+ "os": [
312
+ "aix"
313
+ ],
314
+ "engines": {
315
+ "node": ">=18"
316
+ }
317
+ },
318
+ "node_modules/@esbuild/android-arm": {
319
+ "version": "0.25.12",
320
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
321
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
322
+ "cpu": [
323
+ "arm"
324
+ ],
325
+ "dev": true,
326
+ "license": "MIT",
327
+ "optional": true,
328
+ "os": [
329
+ "android"
330
+ ],
331
+ "engines": {
332
+ "node": ">=18"
333
+ }
334
+ },
335
+ "node_modules/@esbuild/android-arm64": {
336
+ "version": "0.25.12",
337
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
338
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
339
+ "cpu": [
340
+ "arm64"
341
+ ],
342
+ "dev": true,
343
+ "license": "MIT",
344
+ "optional": true,
345
+ "os": [
346
+ "android"
347
+ ],
348
+ "engines": {
349
+ "node": ">=18"
350
+ }
351
+ },
352
+ "node_modules/@esbuild/android-x64": {
353
+ "version": "0.25.12",
354
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
355
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
356
+ "cpu": [
357
+ "x64"
358
+ ],
359
+ "dev": true,
360
+ "license": "MIT",
361
+ "optional": true,
362
+ "os": [
363
+ "android"
364
+ ],
365
+ "engines": {
366
+ "node": ">=18"
367
+ }
368
+ },
369
+ "node_modules/@esbuild/darwin-arm64": {
370
+ "version": "0.25.12",
371
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
372
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
373
+ "cpu": [
374
+ "arm64"
375
+ ],
376
+ "dev": true,
377
+ "license": "MIT",
378
+ "optional": true,
379
+ "os": [
380
+ "darwin"
381
+ ],
382
+ "engines": {
383
+ "node": ">=18"
384
+ }
385
+ },
386
+ "node_modules/@esbuild/darwin-x64": {
387
+ "version": "0.25.12",
388
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
389
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
390
+ "cpu": [
391
+ "x64"
392
+ ],
393
+ "dev": true,
394
+ "license": "MIT",
395
+ "optional": true,
396
+ "os": [
397
+ "darwin"
398
+ ],
399
+ "engines": {
400
+ "node": ">=18"
401
+ }
402
+ },
403
+ "node_modules/@esbuild/freebsd-arm64": {
404
+ "version": "0.25.12",
405
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
406
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
407
+ "cpu": [
408
+ "arm64"
409
+ ],
410
+ "dev": true,
411
+ "license": "MIT",
412
+ "optional": true,
413
+ "os": [
414
+ "freebsd"
415
+ ],
416
+ "engines": {
417
+ "node": ">=18"
418
+ }
419
+ },
420
+ "node_modules/@esbuild/freebsd-x64": {
421
+ "version": "0.25.12",
422
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
423
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
424
+ "cpu": [
425
+ "x64"
426
+ ],
427
+ "dev": true,
428
+ "license": "MIT",
429
+ "optional": true,
430
+ "os": [
431
+ "freebsd"
432
+ ],
433
+ "engines": {
434
+ "node": ">=18"
435
+ }
436
+ },
437
+ "node_modules/@esbuild/linux-arm": {
438
+ "version": "0.25.12",
439
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
440
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
441
+ "cpu": [
442
+ "arm"
443
+ ],
444
+ "dev": true,
445
+ "license": "MIT",
446
+ "optional": true,
447
+ "os": [
448
+ "linux"
449
+ ],
450
+ "engines": {
451
+ "node": ">=18"
452
+ }
453
+ },
454
+ "node_modules/@esbuild/linux-arm64": {
455
+ "version": "0.25.12",
456
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
457
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
458
+ "cpu": [
459
+ "arm64"
460
+ ],
461
+ "dev": true,
462
+ "license": "MIT",
463
+ "optional": true,
464
+ "os": [
465
+ "linux"
466
+ ],
467
+ "engines": {
468
+ "node": ">=18"
469
+ }
470
+ },
471
+ "node_modules/@esbuild/linux-ia32": {
472
+ "version": "0.25.12",
473
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
474
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
475
+ "cpu": [
476
+ "ia32"
477
+ ],
478
+ "dev": true,
479
+ "license": "MIT",
480
+ "optional": true,
481
+ "os": [
482
+ "linux"
483
+ ],
484
+ "engines": {
485
+ "node": ">=18"
486
+ }
487
+ },
488
+ "node_modules/@esbuild/linux-loong64": {
489
+ "version": "0.25.12",
490
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
491
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
492
+ "cpu": [
493
+ "loong64"
494
+ ],
495
+ "dev": true,
496
+ "license": "MIT",
497
+ "optional": true,
498
+ "os": [
499
+ "linux"
500
+ ],
501
+ "engines": {
502
+ "node": ">=18"
503
+ }
504
+ },
505
+ "node_modules/@esbuild/linux-mips64el": {
506
+ "version": "0.25.12",
507
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
508
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
509
+ "cpu": [
510
+ "mips64el"
511
+ ],
512
+ "dev": true,
513
+ "license": "MIT",
514
+ "optional": true,
515
+ "os": [
516
+ "linux"
517
+ ],
518
+ "engines": {
519
+ "node": ">=18"
520
+ }
521
+ },
522
+ "node_modules/@esbuild/linux-ppc64": {
523
+ "version": "0.25.12",
524
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
525
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
526
+ "cpu": [
527
+ "ppc64"
528
+ ],
529
+ "dev": true,
530
+ "license": "MIT",
531
+ "optional": true,
532
+ "os": [
533
+ "linux"
534
+ ],
535
+ "engines": {
536
+ "node": ">=18"
537
+ }
538
+ },
539
+ "node_modules/@esbuild/linux-riscv64": {
540
+ "version": "0.25.12",
541
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
542
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
543
+ "cpu": [
544
+ "riscv64"
545
+ ],
546
+ "dev": true,
547
+ "license": "MIT",
548
+ "optional": true,
549
+ "os": [
550
+ "linux"
551
+ ],
552
+ "engines": {
553
+ "node": ">=18"
554
+ }
555
+ },
556
+ "node_modules/@esbuild/linux-s390x": {
557
+ "version": "0.25.12",
558
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
559
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
560
+ "cpu": [
561
+ "s390x"
562
+ ],
563
+ "dev": true,
564
+ "license": "MIT",
565
+ "optional": true,
566
+ "os": [
567
+ "linux"
568
+ ],
569
+ "engines": {
570
+ "node": ">=18"
571
+ }
572
+ },
573
+ "node_modules/@esbuild/linux-x64": {
574
+ "version": "0.25.12",
575
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
576
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
577
+ "cpu": [
578
+ "x64"
579
+ ],
580
+ "dev": true,
581
+ "license": "MIT",
582
+ "optional": true,
583
+ "os": [
584
+ "linux"
585
+ ],
586
+ "engines": {
587
+ "node": ">=18"
588
+ }
589
+ },
590
+ "node_modules/@esbuild/netbsd-arm64": {
591
+ "version": "0.25.12",
592
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
593
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
594
+ "cpu": [
595
+ "arm64"
596
+ ],
597
+ "dev": true,
598
+ "license": "MIT",
599
+ "optional": true,
600
+ "os": [
601
+ "netbsd"
602
+ ],
603
+ "engines": {
604
+ "node": ">=18"
605
+ }
606
+ },
607
+ "node_modules/@esbuild/netbsd-x64": {
608
+ "version": "0.25.12",
609
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
610
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
611
+ "cpu": [
612
+ "x64"
613
+ ],
614
+ "dev": true,
615
+ "license": "MIT",
616
+ "optional": true,
617
+ "os": [
618
+ "netbsd"
619
+ ],
620
+ "engines": {
621
+ "node": ">=18"
622
+ }
623
+ },
624
+ "node_modules/@esbuild/openbsd-arm64": {
625
+ "version": "0.25.12",
626
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
627
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
628
+ "cpu": [
629
+ "arm64"
630
+ ],
631
+ "dev": true,
632
+ "license": "MIT",
633
+ "optional": true,
634
+ "os": [
635
+ "openbsd"
636
+ ],
637
+ "engines": {
638
+ "node": ">=18"
639
+ }
640
+ },
641
+ "node_modules/@esbuild/openbsd-x64": {
642
+ "version": "0.25.12",
643
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
644
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
645
+ "cpu": [
646
+ "x64"
647
+ ],
648
+ "dev": true,
649
+ "license": "MIT",
650
+ "optional": true,
651
+ "os": [
652
+ "openbsd"
653
+ ],
654
+ "engines": {
655
+ "node": ">=18"
656
+ }
657
+ },
658
+ "node_modules/@esbuild/openharmony-arm64": {
659
+ "version": "0.25.12",
660
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
661
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
662
+ "cpu": [
663
+ "arm64"
664
+ ],
665
+ "dev": true,
666
+ "license": "MIT",
667
+ "optional": true,
668
+ "os": [
669
+ "openharmony"
670
+ ],
671
+ "engines": {
672
+ "node": ">=18"
673
+ }
674
+ },
675
+ "node_modules/@esbuild/sunos-x64": {
676
+ "version": "0.25.12",
677
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
678
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
679
+ "cpu": [
680
+ "x64"
681
+ ],
682
+ "dev": true,
683
+ "license": "MIT",
684
+ "optional": true,
685
+ "os": [
686
+ "sunos"
687
+ ],
688
+ "engines": {
689
+ "node": ">=18"
690
+ }
691
+ },
692
+ "node_modules/@esbuild/win32-arm64": {
693
+ "version": "0.25.12",
694
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
695
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
696
+ "cpu": [
697
+ "arm64"
698
+ ],
699
+ "dev": true,
700
+ "license": "MIT",
701
+ "optional": true,
702
+ "os": [
703
+ "win32"
704
+ ],
705
+ "engines": {
706
+ "node": ">=18"
707
+ }
708
+ },
709
+ "node_modules/@esbuild/win32-ia32": {
710
+ "version": "0.25.12",
711
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
712
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
713
+ "cpu": [
714
+ "ia32"
715
+ ],
716
+ "dev": true,
717
+ "license": "MIT",
718
+ "optional": true,
719
+ "os": [
720
+ "win32"
721
+ ],
722
+ "engines": {
723
+ "node": ">=18"
724
+ }
725
+ },
726
+ "node_modules/@esbuild/win32-x64": {
727
+ "version": "0.25.12",
728
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
729
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
730
+ "cpu": [
731
+ "x64"
732
+ ],
733
+ "dev": true,
734
+ "license": "MIT",
735
+ "optional": true,
736
+ "os": [
737
+ "win32"
738
+ ],
739
+ "engines": {
740
+ "node": ">=18"
741
+ }
742
+ },
743
+ "node_modules/@jridgewell/gen-mapping": {
744
+ "version": "0.3.13",
745
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
746
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
747
+ "dev": true,
748
+ "license": "MIT",
749
+ "dependencies": {
750
+ "@jridgewell/sourcemap-codec": "^1.5.0",
751
+ "@jridgewell/trace-mapping": "^0.3.24"
752
+ }
753
+ },
754
+ "node_modules/@jridgewell/remapping": {
755
+ "version": "2.3.5",
756
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
757
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
758
+ "dev": true,
759
+ "license": "MIT",
760
+ "dependencies": {
761
+ "@jridgewell/gen-mapping": "^0.3.5",
762
+ "@jridgewell/trace-mapping": "^0.3.24"
763
+ }
764
+ },
765
+ "node_modules/@jridgewell/resolve-uri": {
766
+ "version": "3.1.2",
767
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
768
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
769
+ "dev": true,
770
+ "license": "MIT",
771
+ "engines": {
772
+ "node": ">=6.0.0"
773
+ }
774
+ },
775
+ "node_modules/@jridgewell/sourcemap-codec": {
776
+ "version": "1.6.0",
777
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
778
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
779
+ "dev": true,
780
+ "license": "MIT"
781
+ },
782
+ "node_modules/@jridgewell/trace-mapping": {
783
+ "version": "0.3.31",
784
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
785
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
786
+ "dev": true,
787
+ "license": "MIT",
788
+ "dependencies": {
789
+ "@jridgewell/resolve-uri": "^3.1.0",
790
+ "@jridgewell/sourcemap-codec": "^1.4.14"
791
+ }
792
+ },
793
+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
794
+ "version": "1.5.1",
795
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
796
+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
797
+ "cpu": [
798
+ "x64"
799
+ ],
800
+ "dev": true,
801
+ "libc": [
802
+ "glibc"
803
+ ],
804
+ "license": "MIT",
805
+ "optional": true,
806
+ "os": [
807
+ "linux"
808
+ ],
809
+ "engines": {
810
+ "node": "^22.20 || ^24.12 || >=25"
811
+ }
812
+ },
813
+ "node_modules/@rolldown/pluginutils": {
814
+ "version": "1.0.0-beta.27",
815
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
816
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
817
+ "dev": true,
818
+ "license": "MIT"
819
+ },
820
+ "node_modules/@rollup/rollup-android-arm-eabi": {
821
+ "version": "4.65.0",
822
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.65.0.tgz",
823
+ "integrity": "sha512-t2AnVba25baFdq7gczzS13JyVcdubLVbFX1X04mxrggDrTQPUXNv3XM0k9H1hr5tqvg7AVvtQ720QHAj0iYqhg==",
824
+ "cpu": [
825
+ "arm"
826
+ ],
827
+ "dev": true,
828
+ "license": "MIT",
829
+ "optional": true,
830
+ "os": [
831
+ "android"
832
+ ]
833
+ },
834
+ "node_modules/@rollup/rollup-android-arm64": {
835
+ "version": "4.65.0",
836
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.65.0.tgz",
837
+ "integrity": "sha512-dNONUaEwPGhl/nuO2KV8VABWlcjYyU6KtZGN8dQv5OS5JU7JSCxPijVNKXbRJd0iuTzTiJ4TjPt/4bbvDRNiMQ==",
838
+ "cpu": [
839
+ "arm64"
840
+ ],
841
+ "dev": true,
842
+ "license": "MIT",
843
+ "optional": true,
844
+ "os": [
845
+ "android"
846
+ ]
847
+ },
848
+ "node_modules/@rollup/rollup-darwin-arm64": {
849
+ "version": "4.65.0",
850
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.65.0.tgz",
851
+ "integrity": "sha512-zWQqUH+9zavr2Bxuzfpuv2SIF3X31cr58TvSQC9o1GhpOvkqRwr60GkZdQcudyT/BEgdpdSafjgd2I13MbTL7g==",
852
+ "cpu": [
853
+ "arm64"
854
+ ],
855
+ "dev": true,
856
+ "license": "MIT",
857
+ "optional": true,
858
+ "os": [
859
+ "darwin"
860
+ ]
861
+ },
862
+ "node_modules/@rollup/rollup-darwin-x64": {
863
+ "version": "4.65.0",
864
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.65.0.tgz",
865
+ "integrity": "sha512-RQYreBFWtO4//hIuOaZkYUnF/lPU4lz35dveDimDshWCJvVisVtJ/shmT9bkXK8wFJY+FJF89X6F3Cq1qI3LCw==",
866
+ "cpu": [
867
+ "x64"
868
+ ],
869
+ "dev": true,
870
+ "license": "MIT",
871
+ "optional": true,
872
+ "os": [
873
+ "darwin"
874
+ ]
875
+ },
876
+ "node_modules/@rollup/rollup-freebsd-arm64": {
877
+ "version": "4.65.0",
878
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.65.0.tgz",
879
+ "integrity": "sha512-A0NfMj6Cuh/9BE5k3awJKv6HoxFVXIcLAb6SZqAMoxUHPA1ZNNK4RsGuUPD/vL7nEAm9LG+ZzXWWTrnC0XtkpA==",
880
+ "cpu": [
881
+ "arm64"
882
+ ],
883
+ "dev": true,
884
+ "license": "MIT",
885
+ "optional": true,
886
+ "os": [
887
+ "freebsd"
888
+ ]
889
+ },
890
+ "node_modules/@rollup/rollup-freebsd-x64": {
891
+ "version": "4.65.0",
892
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.65.0.tgz",
893
+ "integrity": "sha512-BI6/H7q3aP0I7S7GOrjyxTjgEJlSnovXE38+bdxBpuKN6z+d//uF+T+VQ3Q7ltJL/71p17ufartARX9Ja5gOGQ==",
894
+ "cpu": [
895
+ "x64"
896
+ ],
897
+ "dev": true,
898
+ "license": "MIT",
899
+ "optional": true,
900
+ "os": [
901
+ "freebsd"
902
+ ]
903
+ },
904
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
905
+ "version": "4.65.0",
906
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.65.0.tgz",
907
+ "integrity": "sha512-W1vUprf09RWaVWnsFP1vlUkEWJzTZlpObOEAYZXt9Fq0UVAOYEkeGr7wBX7McKqqIQskXdBnpiOMgwPujordgA==",
908
+ "cpu": [
909
+ "arm"
910
+ ],
911
+ "dev": true,
912
+ "libc": [
913
+ "glibc"
914
+ ],
915
+ "license": "MIT",
916
+ "optional": true,
917
+ "os": [
918
+ "linux"
919
+ ]
920
+ },
921
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
922
+ "version": "4.65.0",
923
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.65.0.tgz",
924
+ "integrity": "sha512-s8QTcZWgrl9haFzEGIqyE5BWyo89pFDGLMBFZ752WYxk8DJwNU/O1X6pZgPw829d9/sm6CCHyrMP3aurt76p1Q==",
925
+ "cpu": [
926
+ "arm"
927
+ ],
928
+ "dev": true,
929
+ "libc": [
930
+ "musl"
931
+ ],
932
+ "license": "MIT",
933
+ "optional": true,
934
+ "os": [
935
+ "linux"
936
+ ]
937
+ },
938
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
939
+ "version": "4.65.0",
940
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.65.0.tgz",
941
+ "integrity": "sha512-ke1f6dLRzkqveyjjF2fkEwQYZvxHtNNwjegY8nJrwcO0Bdqr8CzFO/pKE0Bns13cx0qz2Ba2Y0Hs3dSwMdnHxA==",
942
+ "cpu": [
943
+ "arm64"
944
+ ],
945
+ "dev": true,
946
+ "libc": [
947
+ "glibc"
948
+ ],
949
+ "license": "MIT",
950
+ "optional": true,
951
+ "os": [
952
+ "linux"
953
+ ]
954
+ },
955
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
956
+ "version": "4.65.0",
957
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.65.0.tgz",
958
+ "integrity": "sha512-926SMeUcy4y18Rz/w2aJiUKOnyAnm6czg72bXkQEaeemsll+Wr0hnsjb2BGRpHvWgwAMYy5ACJCotbvfLCfj0A==",
959
+ "cpu": [
960
+ "arm64"
961
+ ],
962
+ "dev": true,
963
+ "libc": [
964
+ "musl"
965
+ ],
966
+ "license": "MIT",
967
+ "optional": true,
968
+ "os": [
969
+ "linux"
970
+ ]
971
+ },
972
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
973
+ "version": "4.65.0",
974
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.65.0.tgz",
975
+ "integrity": "sha512-6T7JNRP1hc4hqF2dUsEoVLOt9K8tmODoCLldtQRYnrddGGQY8Gqe2pM/qMMUo7EMowpP7oa2N95k0/8sYK/ipw==",
976
+ "cpu": [
977
+ "loong64"
978
+ ],
979
+ "dev": true,
980
+ "libc": [
981
+ "glibc"
982
+ ],
983
+ "license": "MIT",
984
+ "optional": true,
985
+ "os": [
986
+ "linux"
987
+ ]
988
+ },
989
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
990
+ "version": "4.65.0",
991
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.65.0.tgz",
992
+ "integrity": "sha512-SImGFl1Je5kpyAgOCPONVT7h6ASV8mAUwGRjY7eZvVjPg//PfHOXgI+P241RF/+HqsA4PP3qD527ysWFKNQyhQ==",
993
+ "cpu": [
994
+ "loong64"
995
+ ],
996
+ "dev": true,
997
+ "libc": [
998
+ "musl"
999
+ ],
1000
+ "license": "MIT",
1001
+ "optional": true,
1002
+ "os": [
1003
+ "linux"
1004
+ ]
1005
+ },
1006
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1007
+ "version": "4.65.0",
1008
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.65.0.tgz",
1009
+ "integrity": "sha512-XeDTbmPF1Yu5asfXGPrMkWoxTDhgryQbmw5Q1+R582O51VNCk23J6f2pDrsAp5YkneBVBwtExx6ZGOb9D2aQ6A==",
1010
+ "cpu": [
1011
+ "ppc64"
1012
+ ],
1013
+ "dev": true,
1014
+ "libc": [
1015
+ "glibc"
1016
+ ],
1017
+ "license": "MIT",
1018
+ "optional": true,
1019
+ "os": [
1020
+ "linux"
1021
+ ]
1022
+ },
1023
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
1024
+ "version": "4.65.0",
1025
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.65.0.tgz",
1026
+ "integrity": "sha512-VS55JUI/1nsljmV/jfqNkLZXFw8uMO8nNlfoHmSevNf6fO3neuhyDvx9dv3c/Ytt8r/431SL8Qax1dpYvOMZTg==",
1027
+ "cpu": [
1028
+ "ppc64"
1029
+ ],
1030
+ "dev": true,
1031
+ "libc": [
1032
+ "musl"
1033
+ ],
1034
+ "license": "MIT",
1035
+ "optional": true,
1036
+ "os": [
1037
+ "linux"
1038
+ ]
1039
+ },
1040
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1041
+ "version": "4.65.0",
1042
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.65.0.tgz",
1043
+ "integrity": "sha512-Vblw84TbX7RS4la1Y4+yQ7aore/oSyaNtFbppuBr5Vs/4LfBeXKq9dIfoExw6zOvRERqd2rRAjnZRaMhH/0x5w==",
1044
+ "cpu": [
1045
+ "riscv64"
1046
+ ],
1047
+ "dev": true,
1048
+ "libc": [
1049
+ "glibc"
1050
+ ],
1051
+ "license": "MIT",
1052
+ "optional": true,
1053
+ "os": [
1054
+ "linux"
1055
+ ]
1056
+ },
1057
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1058
+ "version": "4.65.0",
1059
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.65.0.tgz",
1060
+ "integrity": "sha512-7+gHjtONvKgwR9InTKFaLq53f6YzBJkgeMRBENdWTsQr+spahGqQm8HF5aXpq8i4JaRrOqYyv6Zn1WltDTxaZQ==",
1061
+ "cpu": [
1062
+ "riscv64"
1063
+ ],
1064
+ "dev": true,
1065
+ "libc": [
1066
+ "musl"
1067
+ ],
1068
+ "license": "MIT",
1069
+ "optional": true,
1070
+ "os": [
1071
+ "linux"
1072
+ ]
1073
+ },
1074
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1075
+ "version": "4.65.0",
1076
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.65.0.tgz",
1077
+ "integrity": "sha512-KvNDC0J6s4vVHhtJ6KrDXU0Er0au/hmRIBq/+1maCV8TseGupeKizq0bWcrbp2HE1T8xoknvN0Q/KZ/YF0qX3w==",
1078
+ "cpu": [
1079
+ "s390x"
1080
+ ],
1081
+ "dev": true,
1082
+ "libc": [
1083
+ "glibc"
1084
+ ],
1085
+ "license": "MIT",
1086
+ "optional": true,
1087
+ "os": [
1088
+ "linux"
1089
+ ]
1090
+ },
1091
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1092
+ "version": "4.65.0",
1093
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.65.0.tgz",
1094
+ "integrity": "sha512-GExdD15WPvu/x/drELjVam7vX8EsPgvTMOa/iJL/p/4m/oOqzbb7JXZg8fXR4hlXtg/sF2zyTZ3bR0AEtEMlhQ==",
1095
+ "cpu": [
1096
+ "x64"
1097
+ ],
1098
+ "dev": true,
1099
+ "libc": [
1100
+ "glibc"
1101
+ ],
1102
+ "license": "MIT",
1103
+ "optional": true,
1104
+ "os": [
1105
+ "linux"
1106
+ ]
1107
+ },
1108
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1109
+ "version": "4.65.0",
1110
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.65.0.tgz",
1111
+ "integrity": "sha512-8sT9PemIZfIMED4Vd4KGoOXVDj5CMSWNcGdbheRUx9TYS03yOWdT2Q0lXPyEyNBB9YTilwewIQKNIBk6Nxi0Kg==",
1112
+ "cpu": [
1113
+ "x64"
1114
+ ],
1115
+ "dev": true,
1116
+ "libc": [
1117
+ "musl"
1118
+ ],
1119
+ "license": "MIT",
1120
+ "optional": true,
1121
+ "os": [
1122
+ "linux"
1123
+ ]
1124
+ },
1125
+ "node_modules/@rollup/rollup-openbsd-x64": {
1126
+ "version": "4.65.0",
1127
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.65.0.tgz",
1128
+ "integrity": "sha512-pO6Ehi0jo5yE0R6PltKhJhv9mlIiyzBJk5N5NUF2OF6xtVKI+1bdXxj/rb0jGXy3hck1ELVT0QPY3tzkAm360w==",
1129
+ "cpu": [
1130
+ "x64"
1131
+ ],
1132
+ "dev": true,
1133
+ "license": "MIT",
1134
+ "optional": true,
1135
+ "os": [
1136
+ "openbsd"
1137
+ ]
1138
+ },
1139
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1140
+ "version": "4.65.0",
1141
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.65.0.tgz",
1142
+ "integrity": "sha512-vo7N3/+cjjPDYr8vtxC+qSNgWa9sDvJ7g8N8R5SWqFY4WiNH1ULcAU70SKVyHeiIZOYYwNP9pTipmhN5sl1NPw==",
1143
+ "cpu": [
1144
+ "arm64"
1145
+ ],
1146
+ "dev": true,
1147
+ "license": "MIT",
1148
+ "optional": true,
1149
+ "os": [
1150
+ "openharmony"
1151
+ ]
1152
+ },
1153
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1154
+ "version": "4.65.0",
1155
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.65.0.tgz",
1156
+ "integrity": "sha512-ArwvgFAbjKcDM7qqotZ4OVYf+UMT9xH2YzkCzK4+AuZSDihMlz1Ekoe4INwBVXzn2mnJzuIYOcSx07iTBijGRQ==",
1157
+ "cpu": [
1158
+ "arm64"
1159
+ ],
1160
+ "dev": true,
1161
+ "license": "MIT",
1162
+ "optional": true,
1163
+ "os": [
1164
+ "win32"
1165
+ ]
1166
+ },
1167
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1168
+ "version": "4.65.0",
1169
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.65.0.tgz",
1170
+ "integrity": "sha512-QGMmK4/cxKE/36FoLMSHQBCyymv/eFTnsCvvsBF0rgmOvVbvX78VWZRerL3dHa77f4pM6Xcc2iGDhJ10w/svKQ==",
1171
+ "cpu": [
1172
+ "ia32"
1173
+ ],
1174
+ "dev": true,
1175
+ "license": "MIT",
1176
+ "optional": true,
1177
+ "os": [
1178
+ "win32"
1179
+ ]
1180
+ },
1181
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1182
+ "version": "4.65.0",
1183
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.65.0.tgz",
1184
+ "integrity": "sha512-XfKYFWHIPs4HPODIMsGF1IKlS3H1e3WokUZIgpmn4+uNnZBlnRDDHEylNyazbQXwhMKZre68OjtRYbKqb5+j8w==",
1185
+ "cpu": [
1186
+ "x64"
1187
+ ],
1188
+ "dev": true,
1189
+ "license": "MIT",
1190
+ "optional": true,
1191
+ "os": [
1192
+ "win32"
1193
+ ]
1194
+ },
1195
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1196
+ "version": "4.65.0",
1197
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.65.0.tgz",
1198
+ "integrity": "sha512-wPWi++K4Bs1VR4XihOhfGi4IGX6Cub7FlerTf8X2v+2BtDHddgSqWCXwGD+h2yAe4/niIhNDwAltioOC+wEogQ==",
1199
+ "cpu": [
1200
+ "x64"
1201
+ ],
1202
+ "dev": true,
1203
+ "license": "MIT",
1204
+ "optional": true,
1205
+ "os": [
1206
+ "win32"
1207
+ ]
1208
+ },
1209
+ "node_modules/@types/babel__core": {
1210
+ "version": "7.20.5",
1211
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1212
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1213
+ "dev": true,
1214
+ "license": "MIT",
1215
+ "dependencies": {
1216
+ "@babel/parser": "^7.20.7",
1217
+ "@babel/types": "^7.20.7",
1218
+ "@types/babel__generator": "*",
1219
+ "@types/babel__template": "*",
1220
+ "@types/babel__traverse": "*"
1221
+ }
1222
+ },
1223
+ "node_modules/@types/babel__generator": {
1224
+ "version": "7.27.0",
1225
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1226
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1227
+ "dev": true,
1228
+ "license": "MIT",
1229
+ "dependencies": {
1230
+ "@babel/types": "^7.0.0"
1231
+ }
1232
+ },
1233
+ "node_modules/@types/babel__template": {
1234
+ "version": "7.4.4",
1235
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1236
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1237
+ "dev": true,
1238
+ "license": "MIT",
1239
+ "dependencies": {
1240
+ "@babel/parser": "^7.1.0",
1241
+ "@babel/types": "^7.0.0"
1242
+ }
1243
+ },
1244
+ "node_modules/@types/babel__traverse": {
1245
+ "version": "7.28.0",
1246
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1247
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1248
+ "dev": true,
1249
+ "license": "MIT",
1250
+ "dependencies": {
1251
+ "@babel/types": "^7.28.2"
1252
+ }
1253
+ },
1254
+ "node_modules/@types/estree": {
1255
+ "version": "1.0.9",
1256
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1257
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1258
+ "dev": true,
1259
+ "license": "MIT"
1260
+ },
1261
+ "node_modules/@vitejs/plugin-react": {
1262
+ "version": "4.7.0",
1263
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1264
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1265
+ "dev": true,
1266
+ "license": "MIT",
1267
+ "dependencies": {
1268
+ "@babel/core": "^7.28.0",
1269
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1270
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1271
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1272
+ "@types/babel__core": "^7.20.5",
1273
+ "react-refresh": "^0.17.0"
1274
+ },
1275
+ "engines": {
1276
+ "node": "^14.18.0 || >=16.0.0"
1277
+ },
1278
+ "peerDependencies": {
1279
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1280
+ }
1281
+ },
1282
+ "node_modules/baseline-browser-mapping": {
1283
+ "version": "2.11.32",
1284
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.32.tgz",
1285
+ "integrity": "sha512-UvuxFU9P3hM29LrQdwu0cD6w6/+SFr+UlB9s/knI7+O2rK9xFxKUwSDWxGdbbqQ9/V7BJ5s0NI5nQAE4uJp3gQ==",
1286
+ "dev": true,
1287
+ "license": "Apache-2.0",
1288
+ "bin": {
1289
+ "baseline-browser-mapping": "dist/cli.cjs"
1290
+ },
1291
+ "engines": {
1292
+ "node": ">=6.0.0"
1293
+ }
1294
+ },
1295
+ "node_modules/browserslist": {
1296
+ "version": "4.29.4",
1297
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz",
1298
+ "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==",
1299
+ "dev": true,
1300
+ "funding": [
1301
+ {
1302
+ "type": "opencollective",
1303
+ "url": "https://opencollective.com/browserslist"
1304
+ },
1305
+ {
1306
+ "type": "tidelift",
1307
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1308
+ },
1309
+ {
1310
+ "type": "github",
1311
+ "url": "https://github.com/sponsors/ai"
1312
+ }
1313
+ ],
1314
+ "license": "MIT",
1315
+ "dependencies": {
1316
+ "baseline-browser-mapping": "^2.11.29",
1317
+ "caniuse-lite": "^1.0.30001816",
1318
+ "electron-to-chromium": "^1.5.459",
1319
+ "node-releases": "^2.0.59",
1320
+ "update-browserslist-db": "^1.3.4"
1321
+ },
1322
+ "bin": {
1323
+ "browserslist": "cli.js"
1324
+ },
1325
+ "engines": {
1326
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1327
+ }
1328
+ },
1329
+ "node_modules/caniuse-lite": {
1330
+ "version": "1.0.30001819",
1331
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz",
1332
+ "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==",
1333
+ "dev": true,
1334
+ "funding": [
1335
+ {
1336
+ "type": "opencollective",
1337
+ "url": "https://opencollective.com/browserslist"
1338
+ },
1339
+ {
1340
+ "type": "tidelift",
1341
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1342
+ },
1343
+ {
1344
+ "type": "github",
1345
+ "url": "https://github.com/sponsors/ai"
1346
+ }
1347
+ ],
1348
+ "license": "CC-BY-4.0"
1349
+ },
1350
+ "node_modules/convert-source-map": {
1351
+ "version": "2.0.0",
1352
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1353
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1354
+ "dev": true,
1355
+ "license": "MIT"
1356
+ },
1357
+ "node_modules/debug": {
1358
+ "version": "4.4.3",
1359
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1360
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1361
+ "dev": true,
1362
+ "license": "MIT",
1363
+ "dependencies": {
1364
+ "ms": "^2.1.3"
1365
+ },
1366
+ "engines": {
1367
+ "node": ">=6.0"
1368
+ },
1369
+ "peerDependenciesMeta": {
1370
+ "supports-color": {
1371
+ "optional": true
1372
+ }
1373
+ }
1374
+ },
1375
+ "node_modules/electron-to-chromium": {
1376
+ "version": "1.5.464",
1377
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz",
1378
+ "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==",
1379
+ "dev": true,
1380
+ "license": "ISC"
1381
+ },
1382
+ "node_modules/esbuild": {
1383
+ "version": "0.25.12",
1384
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
1385
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1386
+ "dev": true,
1387
+ "hasInstallScript": true,
1388
+ "license": "MIT",
1389
+ "bin": {
1390
+ "esbuild": "bin/esbuild"
1391
+ },
1392
+ "engines": {
1393
+ "node": ">=18"
1394
+ },
1395
+ "optionalDependencies": {
1396
+ "@esbuild/aix-ppc64": "0.25.12",
1397
+ "@esbuild/android-arm": "0.25.12",
1398
+ "@esbuild/android-arm64": "0.25.12",
1399
+ "@esbuild/android-x64": "0.25.12",
1400
+ "@esbuild/darwin-arm64": "0.25.12",
1401
+ "@esbuild/darwin-x64": "0.25.12",
1402
+ "@esbuild/freebsd-arm64": "0.25.12",
1403
+ "@esbuild/freebsd-x64": "0.25.12",
1404
+ "@esbuild/linux-arm": "0.25.12",
1405
+ "@esbuild/linux-arm64": "0.25.12",
1406
+ "@esbuild/linux-ia32": "0.25.12",
1407
+ "@esbuild/linux-loong64": "0.25.12",
1408
+ "@esbuild/linux-mips64el": "0.25.12",
1409
+ "@esbuild/linux-ppc64": "0.25.12",
1410
+ "@esbuild/linux-riscv64": "0.25.12",
1411
+ "@esbuild/linux-s390x": "0.25.12",
1412
+ "@esbuild/linux-x64": "0.25.12",
1413
+ "@esbuild/netbsd-arm64": "0.25.12",
1414
+ "@esbuild/netbsd-x64": "0.25.12",
1415
+ "@esbuild/openbsd-arm64": "0.25.12",
1416
+ "@esbuild/openbsd-x64": "0.25.12",
1417
+ "@esbuild/openharmony-arm64": "0.25.12",
1418
+ "@esbuild/sunos-x64": "0.25.12",
1419
+ "@esbuild/win32-arm64": "0.25.12",
1420
+ "@esbuild/win32-ia32": "0.25.12",
1421
+ "@esbuild/win32-x64": "0.25.12"
1422
+ }
1423
+ },
1424
+ "node_modules/escalade": {
1425
+ "version": "3.2.0",
1426
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1427
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1428
+ "dev": true,
1429
+ "license": "MIT",
1430
+ "engines": {
1431
+ "node": ">=6"
1432
+ }
1433
+ },
1434
+ "node_modules/fdir": {
1435
+ "version": "6.5.0",
1436
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1437
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1438
+ "dev": true,
1439
+ "license": "MIT",
1440
+ "engines": {
1441
+ "node": ">=12.0.0"
1442
+ },
1443
+ "peerDependencies": {
1444
+ "picomatch": "^3 || ^4"
1445
+ },
1446
+ "peerDependenciesMeta": {
1447
+ "picomatch": {
1448
+ "optional": true
1449
+ }
1450
+ }
1451
+ },
1452
+ "node_modules/fsevents": {
1453
+ "version": "2.3.3",
1454
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1455
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1456
+ "dev": true,
1457
+ "hasInstallScript": true,
1458
+ "license": "MIT",
1459
+ "optional": true,
1460
+ "os": [
1461
+ "darwin"
1462
+ ],
1463
+ "engines": {
1464
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1465
+ }
1466
+ },
1467
+ "node_modules/gensync": {
1468
+ "version": "1.0.0-beta.2",
1469
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1470
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1471
+ "dev": true,
1472
+ "license": "MIT",
1473
+ "engines": {
1474
+ "node": ">=6.9.0"
1475
+ }
1476
+ },
1477
+ "node_modules/js-tokens": {
1478
+ "version": "4.0.0",
1479
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1480
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1481
+ "dev": true,
1482
+ "license": "MIT"
1483
+ },
1484
+ "node_modules/jsesc": {
1485
+ "version": "3.1.0",
1486
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1487
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1488
+ "dev": true,
1489
+ "license": "MIT",
1490
+ "bin": {
1491
+ "jsesc": "bin/jsesc"
1492
+ },
1493
+ "engines": {
1494
+ "node": ">=6"
1495
+ }
1496
+ },
1497
+ "node_modules/json5": {
1498
+ "version": "2.2.3",
1499
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1500
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1501
+ "dev": true,
1502
+ "license": "MIT",
1503
+ "bin": {
1504
+ "json5": "lib/cli.js"
1505
+ },
1506
+ "engines": {
1507
+ "node": ">=6"
1508
+ }
1509
+ },
1510
+ "node_modules/lru-cache": {
1511
+ "version": "5.1.1",
1512
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1513
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1514
+ "dev": true,
1515
+ "license": "ISC",
1516
+ "dependencies": {
1517
+ "yallist": "^3.0.2"
1518
+ }
1519
+ },
1520
+ "node_modules/ms": {
1521
+ "version": "2.1.3",
1522
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1523
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1524
+ "dev": true,
1525
+ "license": "MIT"
1526
+ },
1527
+ "node_modules/nanoid": {
1528
+ "version": "3.3.20",
1529
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
1530
+ "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
1531
+ "dev": true,
1532
+ "funding": [
1533
+ {
1534
+ "type": "github",
1535
+ "url": "https://github.com/sponsors/ai"
1536
+ }
1537
+ ],
1538
+ "license": "MIT",
1539
+ "bin": {
1540
+ "nanoid": "bin/nanoid.cjs"
1541
+ },
1542
+ "engines": {
1543
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1544
+ }
1545
+ },
1546
+ "node_modules/node-releases": {
1547
+ "version": "2.0.60",
1548
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz",
1549
+ "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==",
1550
+ "dev": true,
1551
+ "license": "MIT",
1552
+ "engines": {
1553
+ "node": ">=18"
1554
+ }
1555
+ },
1556
+ "node_modules/picocolors": {
1557
+ "version": "1.1.1",
1558
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1559
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1560
+ "dev": true,
1561
+ "license": "ISC"
1562
+ },
1563
+ "node_modules/picomatch": {
1564
+ "version": "4.0.7",
1565
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
1566
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
1567
+ "dev": true,
1568
+ "license": "MIT",
1569
+ "engines": {
1570
+ "node": ">=12"
1571
+ },
1572
+ "funding": {
1573
+ "url": "https://github.com/sponsors/jonschlinkert"
1574
+ }
1575
+ },
1576
+ "node_modules/postcss": {
1577
+ "version": "8.5.31",
1578
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
1579
+ "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
1580
+ "dev": true,
1581
+ "funding": [
1582
+ {
1583
+ "type": "opencollective",
1584
+ "url": "https://opencollective.com/postcss/"
1585
+ },
1586
+ {
1587
+ "type": "tidelift",
1588
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1589
+ },
1590
+ {
1591
+ "type": "github",
1592
+ "url": "https://github.com/sponsors/ai"
1593
+ }
1594
+ ],
1595
+ "license": "MIT",
1596
+ "dependencies": {
1597
+ "nanoid": "^3.3.20",
1598
+ "picocolors": "^1.1.1",
1599
+ "source-map-js": "^1.2.2"
1600
+ },
1601
+ "engines": {
1602
+ "node": "^10 || ^12 || >=14"
1603
+ }
1604
+ },
1605
+ "node_modules/react": {
1606
+ "version": "19.3.1",
1607
+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz",
1608
+ "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==",
1609
+ "license": "MIT",
1610
+ "engines": {
1611
+ "node": ">=0.10.0"
1612
+ }
1613
+ },
1614
+ "node_modules/react-dom": {
1615
+ "version": "19.3.1",
1616
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz",
1617
+ "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==",
1618
+ "license": "MIT",
1619
+ "dependencies": {
1620
+ "scheduler": "^0.28.0"
1621
+ },
1622
+ "peerDependencies": {
1623
+ "react": "^19.3.1"
1624
+ }
1625
+ },
1626
+ "node_modules/react-refresh": {
1627
+ "version": "0.17.0",
1628
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1629
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1630
+ "dev": true,
1631
+ "license": "MIT",
1632
+ "engines": {
1633
+ "node": ">=0.10.0"
1634
+ }
1635
+ },
1636
+ "node_modules/rollup": {
1637
+ "version": "4.65.0",
1638
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.65.0.tgz",
1639
+ "integrity": "sha512-QVCvQUTGDN6JRwi3jVGCY3H4aTHvi9rX0LyoBMkGUvdh/Z6O51CzcLz85BWZaTCDGhl6ySGqYZs+xI1jmPqGcw==",
1640
+ "dev": true,
1641
+ "license": "MIT",
1642
+ "dependencies": {
1643
+ "@types/estree": "1.0.9"
1644
+ },
1645
+ "bin": {
1646
+ "rollup": "dist/bin/rollup"
1647
+ },
1648
+ "engines": {
1649
+ "node": ">=18.0.0",
1650
+ "npm": ">=8.0.0"
1651
+ },
1652
+ "optionalDependencies": {
1653
+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
1654
+ "@rollup/rollup-android-arm-eabi": "4.65.0",
1655
+ "@rollup/rollup-android-arm64": "4.65.0",
1656
+ "@rollup/rollup-darwin-arm64": "4.65.0",
1657
+ "@rollup/rollup-darwin-x64": "4.65.0",
1658
+ "@rollup/rollup-freebsd-arm64": "4.65.0",
1659
+ "@rollup/rollup-freebsd-x64": "4.65.0",
1660
+ "@rollup/rollup-linux-arm-gnueabihf": "4.65.0",
1661
+ "@rollup/rollup-linux-arm-musleabihf": "4.65.0",
1662
+ "@rollup/rollup-linux-arm64-gnu": "4.65.0",
1663
+ "@rollup/rollup-linux-arm64-musl": "4.65.0",
1664
+ "@rollup/rollup-linux-loong64-gnu": "4.65.0",
1665
+ "@rollup/rollup-linux-loong64-musl": "4.65.0",
1666
+ "@rollup/rollup-linux-ppc64-gnu": "4.65.0",
1667
+ "@rollup/rollup-linux-ppc64-musl": "4.65.0",
1668
+ "@rollup/rollup-linux-riscv64-gnu": "4.65.0",
1669
+ "@rollup/rollup-linux-riscv64-musl": "4.65.0",
1670
+ "@rollup/rollup-linux-s390x-gnu": "4.65.0",
1671
+ "@rollup/rollup-linux-x64-gnu": "4.65.0",
1672
+ "@rollup/rollup-linux-x64-musl": "4.65.0",
1673
+ "@rollup/rollup-openbsd-x64": "4.65.0",
1674
+ "@rollup/rollup-openharmony-arm64": "4.65.0",
1675
+ "@rollup/rollup-win32-arm64-msvc": "4.65.0",
1676
+ "@rollup/rollup-win32-ia32-msvc": "4.65.0",
1677
+ "@rollup/rollup-win32-x64-gnu": "4.65.0",
1678
+ "@rollup/rollup-win32-x64-msvc": "4.65.0",
1679
+ "fsevents": "~2.3.2"
1680
+ }
1681
+ },
1682
+ "node_modules/scheduler": {
1683
+ "version": "0.28.0",
1684
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
1685
+ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
1686
+ "license": "MIT"
1687
+ },
1688
+ "node_modules/semver": {
1689
+ "version": "6.3.1",
1690
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1691
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1692
+ "dev": true,
1693
+ "license": "ISC",
1694
+ "bin": {
1695
+ "semver": "bin/semver.js"
1696
+ }
1697
+ },
1698
+ "node_modules/source-map-js": {
1699
+ "version": "1.2.2",
1700
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
1701
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
1702
+ "dev": true,
1703
+ "license": "BSD-3-Clause",
1704
+ "engines": {
1705
+ "node": ">=0.10.0"
1706
+ }
1707
+ },
1708
+ "node_modules/tinyglobby": {
1709
+ "version": "0.2.17",
1710
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
1711
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
1712
+ "dev": true,
1713
+ "license": "MIT",
1714
+ "dependencies": {
1715
+ "fdir": "^6.5.0",
1716
+ "picomatch": "^4.0.4"
1717
+ },
1718
+ "engines": {
1719
+ "node": ">=12.0.0"
1720
+ },
1721
+ "funding": {
1722
+ "url": "https://github.com/sponsors/SuperchupuDev"
1723
+ }
1724
+ },
1725
+ "node_modules/update-browserslist-db": {
1726
+ "version": "1.3.4",
1727
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz",
1728
+ "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==",
1729
+ "dev": true,
1730
+ "funding": [
1731
+ {
1732
+ "type": "opencollective",
1733
+ "url": "https://opencollective.com/browserslist"
1734
+ },
1735
+ {
1736
+ "type": "tidelift",
1737
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1738
+ },
1739
+ {
1740
+ "type": "github",
1741
+ "url": "https://github.com/sponsors/ai"
1742
+ }
1743
+ ],
1744
+ "license": "MIT",
1745
+ "dependencies": {
1746
+ "escalade": "^3.2.0",
1747
+ "picocolors": "^1.1.1"
1748
+ },
1749
+ "bin": {
1750
+ "update-browserslist-db": "cli.js"
1751
+ },
1752
+ "peerDependencies": {
1753
+ "browserslist": ">= 4.21.0"
1754
+ }
1755
+ },
1756
+ "node_modules/vite": {
1757
+ "version": "6.4.5",
1758
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.5.tgz",
1759
+ "integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==",
1760
+ "dev": true,
1761
+ "license": "MIT",
1762
+ "dependencies": {
1763
+ "esbuild": "^0.25.0",
1764
+ "fdir": "^6.4.4",
1765
+ "picomatch": "^4.0.2",
1766
+ "postcss": "^8.5.3",
1767
+ "rollup": "^4.34.9",
1768
+ "tinyglobby": "^0.2.13"
1769
+ },
1770
+ "bin": {
1771
+ "vite": "bin/vite.js"
1772
+ },
1773
+ "engines": {
1774
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
1775
+ },
1776
+ "funding": {
1777
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1778
+ },
1779
+ "optionalDependencies": {
1780
+ "fsevents": "~2.3.3"
1781
+ },
1782
+ "peerDependencies": {
1783
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
1784
+ "jiti": ">=1.21.0",
1785
+ "less": "*",
1786
+ "lightningcss": "^1.21.0",
1787
+ "sass": "*",
1788
+ "sass-embedded": "*",
1789
+ "stylus": "*",
1790
+ "sugarss": "*",
1791
+ "terser": "^5.16.0",
1792
+ "tsx": "^4.8.1",
1793
+ "yaml": "^2.4.2"
1794
+ },
1795
+ "peerDependenciesMeta": {
1796
+ "@types/node": {
1797
+ "optional": true
1798
+ },
1799
+ "jiti": {
1800
+ "optional": true
1801
+ },
1802
+ "less": {
1803
+ "optional": true
1804
+ },
1805
+ "lightningcss": {
1806
+ "optional": true
1807
+ },
1808
+ "sass": {
1809
+ "optional": true
1810
+ },
1811
+ "sass-embedded": {
1812
+ "optional": true
1813
+ },
1814
+ "stylus": {
1815
+ "optional": true
1816
+ },
1817
+ "sugarss": {
1818
+ "optional": true
1819
+ },
1820
+ "terser": {
1821
+ "optional": true
1822
+ },
1823
+ "tsx": {
1824
+ "optional": true
1825
+ },
1826
+ "yaml": {
1827
+ "optional": true
1828
+ }
1829
+ }
1830
+ },
1831
+ "node_modules/yallist": {
1832
+ "version": "3.1.1",
1833
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1834
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1835
+ "dev": true,
1836
+ "license": "ISC"
1837
+ }
1838
+ }
1839
+ }
projects/telegram-send-demo/frontend/package.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "telegram-demo-frontend",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^19.1.0",
13
+ "react-dom": "^19.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-react": "^4.4.1",
17
+ "vite": "^6.3.5"
18
+ }
19
+ }
projects/telegram-send-demo/frontend/public/favicon.svg ADDED
projects/telegram-send-demo/frontend/src/App.css ADDED
@@ -0,0 +1,556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --tg-blue: #2aabee;
3
+ --tg-blue-dark: #1d8fc9;
4
+ --ink: #10212b;
5
+ --muted: #5c7480;
6
+ --line: #dbe6ec;
7
+ --surface: #ffffff;
8
+ --success-bg: #e7f8ef;
9
+ --success-ink: #147a4b;
10
+ --error-bg: #fdeaea;
11
+ --error-ink: #b03434;
12
+ --pending-bg: #eef3f6;
13
+ --pending-ink: #48626f;
14
+ color-scheme: light;
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ body {
22
+ margin: 0;
23
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
24
+ color: var(--ink);
25
+ background:
26
+ radial-gradient(circle at 15% 0%, rgba(42, 171, 238, 0.16), transparent 45%),
27
+ radial-gradient(circle at 90% 100%, rgba(122, 92, 250, 0.12), transparent 40%),
28
+ #f4f8fb;
29
+ -webkit-font-smoothing: antialiased;
30
+ }
31
+
32
+ button,
33
+ input,
34
+ textarea {
35
+ font: inherit;
36
+ }
37
+
38
+ .page {
39
+ min-height: 100vh;
40
+ display: grid;
41
+ place-items: center;
42
+ padding: 40px 20px;
43
+ }
44
+
45
+ .card {
46
+ width: 100%;
47
+ max-width: 560px;
48
+ background: var(--surface);
49
+ border: 1px solid var(--line);
50
+ border-radius: 24px;
51
+ padding: 32px;
52
+ box-shadow:
53
+ 0 1px 2px rgba(16, 33, 43, 0.04),
54
+ 0 24px 60px -24px rgba(16, 33, 43, 0.22);
55
+ }
56
+
57
+ /* Header */
58
+
59
+ .card__header {
60
+ display: flex;
61
+ align-items: flex-start;
62
+ gap: 14px;
63
+ margin-bottom: 28px;
64
+ }
65
+
66
+ .brand {
67
+ flex: none;
68
+ display: grid;
69
+ place-items: center;
70
+ width: 44px;
71
+ height: 44px;
72
+ border-radius: 14px;
73
+ font-size: 21px;
74
+ background: linear-gradient(150deg, #47c2ff, var(--tg-blue-dark));
75
+ box-shadow: 0 8px 18px -8px rgba(42, 171, 238, 0.9);
76
+ }
77
+
78
+ .card__header > div:nth-child(2) {
79
+ flex: 1;
80
+ min-width: 0;
81
+ }
82
+
83
+ h1 {
84
+ margin: 1px 0 4px;
85
+ font-size: 1.32rem;
86
+ font-weight: 650;
87
+ letter-spacing: -0.02em;
88
+ }
89
+
90
+ .subtitle {
91
+ margin: 0;
92
+ color: var(--muted);
93
+ font-size: 0.86rem;
94
+ line-height: 1.45;
95
+ }
96
+
97
+ .pill {
98
+ flex: none;
99
+ display: inline-flex;
100
+ align-items: center;
101
+ gap: 6px;
102
+ padding: 5px 10px;
103
+ border-radius: 999px;
104
+ font-size: 0.72rem;
105
+ font-weight: 600;
106
+ white-space: nowrap;
107
+ background: var(--pending-bg);
108
+ color: var(--pending-ink);
109
+ }
110
+
111
+ .pill__dot {
112
+ width: 6px;
113
+ height: 6px;
114
+ border-radius: 50%;
115
+ background: currentColor;
116
+ }
117
+
118
+ .pill--online {
119
+ background: var(--success-bg);
120
+ color: var(--success-ink);
121
+ }
122
+
123
+ .pill--offline {
124
+ background: var(--error-bg);
125
+ color: var(--error-ink);
126
+ }
127
+
128
+ /* Form */
129
+
130
+ .form {
131
+ display: flex;
132
+ flex-direction: column;
133
+ gap: 20px;
134
+ }
135
+
136
+ .field {
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: 8px;
140
+ }
141
+
142
+ .field__label {
143
+ font-size: 0.83rem;
144
+ font-weight: 600;
145
+ letter-spacing: 0.01em;
146
+ }
147
+
148
+ /* Connect Telegram */
149
+
150
+ .connect {
151
+ display: flex;
152
+ flex-direction: column;
153
+ gap: 10px;
154
+ padding: 14px;
155
+ border: 1px dashed #c8dde9;
156
+ border-radius: 14px;
157
+ background: #f8fcfe;
158
+ }
159
+
160
+ .connect--pending {
161
+ border-style: solid;
162
+ border-color: #cfe3ef;
163
+ background: #f2f9fd;
164
+ }
165
+
166
+ .connect__text {
167
+ margin: 0;
168
+ font-size: 0.82rem;
169
+ line-height: 1.5;
170
+ color: var(--muted);
171
+ }
172
+
173
+ .connect__text strong {
174
+ color: var(--ink);
175
+ }
176
+
177
+ .connect__actions {
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: space-between;
181
+ gap: 12px;
182
+ }
183
+
184
+ .text-link {
185
+ font-size: 0.78rem;
186
+ font-weight: 600;
187
+ color: var(--tg-blue-dark);
188
+ text-decoration: none;
189
+ }
190
+
191
+ .text-link:hover {
192
+ text-decoration: underline;
193
+ }
194
+
195
+ .text-button {
196
+ padding: 4px 2px;
197
+ border: 0;
198
+ background: none;
199
+ color: var(--muted);
200
+ font-size: 0.76rem;
201
+ font-weight: 600;
202
+ cursor: pointer;
203
+ text-decoration: underline;
204
+ text-underline-offset: 2px;
205
+ }
206
+
207
+ .text-button:hover {
208
+ color: var(--ink);
209
+ }
210
+
211
+ .connected {
212
+ display: flex;
213
+ align-items: center;
214
+ gap: 11px;
215
+ padding: 12px 14px;
216
+ border: 1px solid #c6ecd8;
217
+ border-radius: 12px;
218
+ background: var(--success-bg);
219
+ }
220
+
221
+ .connected__dot {
222
+ flex: none;
223
+ width: 8px;
224
+ height: 8px;
225
+ border-radius: 50%;
226
+ background: var(--success-ink);
227
+ box-shadow: 0 0 0 4px rgba(20, 122, 75, 0.12);
228
+ }
229
+
230
+ .connected__text {
231
+ flex: 1;
232
+ display: flex;
233
+ flex-direction: column;
234
+ gap: 2px;
235
+ font-size: 0.85rem;
236
+ color: var(--success-ink);
237
+ }
238
+
239
+ .connected__text .hint {
240
+ font-size: 0.74rem;
241
+ color: var(--success-ink);
242
+ opacity: 0.8;
243
+ }
244
+
245
+ .connected .text-button {
246
+ color: var(--success-ink);
247
+ opacity: 0.8;
248
+ }
249
+
250
+ .connected .text-button:hover {
251
+ opacity: 1;
252
+ }
253
+
254
+ /* Manual Chat ID fallback */
255
+
256
+ .manual {
257
+ padding-top: 2px;
258
+ }
259
+
260
+ .manual summary {
261
+ display: inline-flex;
262
+ align-items: center;
263
+ gap: 6px;
264
+ font-size: 0.78rem;
265
+ font-weight: 600;
266
+ color: var(--muted);
267
+ cursor: pointer;
268
+ list-style: none;
269
+ user-select: none;
270
+ }
271
+
272
+ .manual summary::-webkit-details-marker {
273
+ display: none;
274
+ }
275
+
276
+ .manual summary::before {
277
+ content: '▸';
278
+ font-size: 0.7rem;
279
+ transition: transform 120ms ease;
280
+ }
281
+
282
+ .manual[open] summary::before {
283
+ transform: rotate(90deg);
284
+ }
285
+
286
+ .manual summary:hover {
287
+ color: var(--ink);
288
+ }
289
+
290
+ .manual summary:focus-visible {
291
+ outline: 2px solid rgba(42, 171, 238, 0.4);
292
+ outline-offset: 3px;
293
+ border-radius: 4px;
294
+ }
295
+
296
+ .manual__field {
297
+ margin-top: 12px;
298
+ padding: 14px;
299
+ border: 1px solid var(--line);
300
+ border-radius: 12px;
301
+ background: #fbfdfe;
302
+ }
303
+
304
+
305
+ label {
306
+ font-size: 0.83rem;
307
+ font-weight: 600;
308
+ letter-spacing: 0.01em;
309
+ }
310
+
311
+ .label-row {
312
+ display: flex;
313
+ align-items: baseline;
314
+ justify-content: space-between;
315
+ gap: 12px;
316
+ }
317
+
318
+ .counter {
319
+ font-size: 0.72rem;
320
+ font-variant-numeric: tabular-nums;
321
+ color: var(--muted);
322
+ }
323
+
324
+ .counter--over {
325
+ color: var(--error-ink);
326
+ }
327
+
328
+ input,
329
+ textarea {
330
+ width: 100%;
331
+ padding: 11px 14px;
332
+ border: 1px solid var(--line);
333
+ border-radius: 12px;
334
+ background: #fbfdfe;
335
+ color: var(--ink);
336
+ transition:
337
+ border-color 120ms ease,
338
+ box-shadow 120ms ease,
339
+ background-color 120ms ease;
340
+ }
341
+
342
+ input::placeholder,
343
+ textarea::placeholder {
344
+ color: #9db1bb;
345
+ }
346
+
347
+ input:hover,
348
+ textarea:hover {
349
+ border-color: #c3d5df;
350
+ }
351
+
352
+ input:focus-visible,
353
+ textarea:focus-visible {
354
+ outline: none;
355
+ border-color: var(--tg-blue);
356
+ background: #ffffff;
357
+ box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.18);
358
+ }
359
+
360
+ textarea {
361
+ resize: vertical;
362
+ min-height: 120px;
363
+ line-height: 1.5;
364
+ }
365
+
366
+ .hint {
367
+ margin: 0;
368
+ font-size: 0.76rem;
369
+ line-height: 1.5;
370
+ color: var(--muted);
371
+ }
372
+
373
+ .hint strong {
374
+ color: var(--ink);
375
+ font-weight: 600;
376
+ }
377
+
378
+ .button {
379
+ padding: 12px 18px;
380
+ border: 0;
381
+ border-radius: 12px;
382
+ background: linear-gradient(180deg, #38b4f2, var(--tg-blue));
383
+ color: #ffffff;
384
+ font-weight: 650;
385
+ letter-spacing: 0.01em;
386
+ cursor: pointer;
387
+ box-shadow: 0 10px 20px -12px rgba(29, 143, 201, 0.9);
388
+ transition:
389
+ transform 120ms ease,
390
+ filter 120ms ease,
391
+ box-shadow 120ms ease;
392
+ }
393
+
394
+ .button:hover:not(:disabled) {
395
+ filter: brightness(1.05);
396
+ box-shadow: 0 14px 24px -12px rgba(29, 143, 201, 0.95);
397
+ }
398
+
399
+ .button:active:not(:disabled) {
400
+ transform: translateY(1px);
401
+ }
402
+
403
+ .button:focus-visible {
404
+ outline: 3px solid rgba(42, 171, 238, 0.4);
405
+ outline-offset: 2px;
406
+ }
407
+
408
+ .button:disabled {
409
+ cursor: progress;
410
+ filter: saturate(0.55);
411
+ opacity: 0.8;
412
+ }
413
+
414
+ .button--secondary {
415
+ background: #ffffff;
416
+ color: var(--tg-blue-dark);
417
+ border: 1px solid #bfe1f2;
418
+ box-shadow: 0 6px 14px -10px rgba(29, 143, 201, 0.7);
419
+ }
420
+
421
+ .button--secondary:hover:not(:disabled) {
422
+ background: #f4fbff;
423
+ filter: none;
424
+ }
425
+
426
+ /* Status badge */
427
+
428
+ .status {
429
+ min-height: 30px;
430
+ }
431
+
432
+ .badge {
433
+ display: inline-flex;
434
+ align-items: flex-start;
435
+ gap: 8px;
436
+ padding: 8px 12px;
437
+ border-radius: 10px;
438
+ font-size: 0.8rem;
439
+ font-weight: 500;
440
+ line-height: 1.45;
441
+ animation: badge-in 160ms ease-out;
442
+ }
443
+
444
+ .badge > span[aria-hidden] {
445
+ font-weight: 700;
446
+ }
447
+
448
+ .badge--success {
449
+ background: var(--success-bg);
450
+ color: var(--success-ink);
451
+ }
452
+
453
+ .badge--error {
454
+ background: var(--error-bg);
455
+ color: var(--error-ink);
456
+ }
457
+
458
+ .badge--pending {
459
+ background: var(--pending-bg);
460
+ color: var(--pending-ink);
461
+ }
462
+
463
+ @keyframes badge-in {
464
+ from {
465
+ opacity: 0;
466
+ transform: translateY(-3px);
467
+ }
468
+ to {
469
+ opacity: 1;
470
+ transform: translateY(0);
471
+ }
472
+ }
473
+
474
+ /* Preview */
475
+
476
+ .card__footer {
477
+ margin-top: 28px;
478
+ padding-top: 22px;
479
+ border-top: 1px solid var(--line);
480
+ }
481
+
482
+ .preview-label {
483
+ display: block;
484
+ margin-bottom: 10px;
485
+ font-size: 0.7rem;
486
+ font-weight: 600;
487
+ text-transform: uppercase;
488
+ letter-spacing: 0.09em;
489
+ color: var(--muted);
490
+ }
491
+
492
+ .bubble {
493
+ max-width: 88%;
494
+ padding: 10px 12px 8px;
495
+ border-radius: 14px 14px 14px 4px;
496
+ background: #eef6fb;
497
+ border: 1px solid #d9eaf4;
498
+ }
499
+
500
+ .bubble p {
501
+ margin: 0 0 10px;
502
+ font-size: 0.85rem;
503
+ line-height: 1.5;
504
+ white-space: pre-wrap;
505
+ overflow-wrap: anywhere;
506
+ }
507
+
508
+ .keyboard {
509
+ display: flex;
510
+ gap: 6px;
511
+ }
512
+
513
+ .keyboard span {
514
+ flex: 1;
515
+ padding: 7px 6px;
516
+ border-radius: 8px;
517
+ background: #ffffff;
518
+ border: 1px solid #cfe3ef;
519
+ color: var(--tg-blue-dark);
520
+ font-size: 0.75rem;
521
+ font-weight: 600;
522
+ text-align: center;
523
+ white-space: nowrap;
524
+ }
525
+
526
+ @media (max-width: 480px) {
527
+ .page {
528
+ padding: 20px 12px;
529
+ }
530
+
531
+ .card {
532
+ padding: 22px;
533
+ border-radius: 20px;
534
+ }
535
+
536
+ .card__header {
537
+ flex-wrap: wrap;
538
+ }
539
+
540
+ .pill {
541
+ order: 3;
542
+ }
543
+
544
+ .keyboard span {
545
+ font-size: 0.7rem;
546
+ }
547
+ }
548
+
549
+ @media (prefers-reduced-motion: reduce) {
550
+ *,
551
+ *::before,
552
+ *::after {
553
+ animation-duration: 0.01ms !important;
554
+ transition-duration: 0.01ms !important;
555
+ }
556
+ }
projects/telegram-send-demo/frontend/src/App.jsx ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from 'react';
2
+ import './App.css';
3
+
4
+ const API_BASE = import.meta.env.VITE_API_URL ?? '';
5
+ const CHAT_ID_KEY = 'telegram-demo:chat-id';
6
+ const LINKED_KEY = 'telegram-demo:linked';
7
+ const MAX_MESSAGE_LENGTH = 4096;
8
+
9
+ /** Nest validation errors arrive as string or string[]; flatten either. */
10
+ function readErrorMessage(payload, fallback) {
11
+ if (!payload) return fallback;
12
+ if (Array.isArray(payload.message)) return payload.message.join(' ');
13
+ if (typeof payload.message === 'string') return payload.message;
14
+ return fallback;
15
+ }
16
+
17
+ function storedChatId() {
18
+ return localStorage.getItem(CHAT_ID_KEY) ?? '';
19
+ }
20
+
21
+ export default function App() {
22
+ const [chatId, setChatId] = useState(storedChatId);
23
+ const [linked, setLinked] = useState(
24
+ () => localStorage.getItem(LINKED_KEY) === '1' && /^-?\d+$/.test(storedChatId()),
25
+ );
26
+ const [message, setMessage] = useState('Test notification');
27
+ const [status, setStatus] = useState(null);
28
+ const [sending, setSending] = useState(false);
29
+ const [connecting, setConnecting] = useState(false);
30
+ const [pendingLink, setPendingLink] = useState(null);
31
+ const [botInfo, setBotInfo] = useState(null);
32
+ const [apiOnline, setApiOnline] = useState(null);
33
+
34
+ // Health + bot identity, so the button can name the bot before it is clicked.
35
+ useEffect(() => {
36
+ const controller = new AbortController();
37
+
38
+ fetch(`${API_BASE}/api/telegram/health`, { signal: controller.signal })
39
+ .then((response) => setApiOnline(response.ok))
40
+ .catch(() => setApiOnline(false));
41
+
42
+ fetch(`${API_BASE}/api/telegram/bot`, { signal: controller.signal })
43
+ .then((response) => (response.ok ? response.json() : null))
44
+ .then((data) => setBotInfo(data))
45
+ .catch(() => setBotInfo(null));
46
+
47
+ return () => controller.abort();
48
+ }, []);
49
+
50
+ // Poll while the user is in Telegram pressing Start.
51
+ useEffect(() => {
52
+ if (!pendingLink) return undefined;
53
+
54
+ let cancelled = false;
55
+
56
+ const timer = setInterval(async () => {
57
+ try {
58
+ const response = await fetch(`${API_BASE}/api/telegram/link/${pendingLink.code}`);
59
+ const payload = await response.json().catch(() => null);
60
+
61
+ if (cancelled || !payload) return;
62
+
63
+ if (payload.status === 'connected') {
64
+ const connectedChatId = String(payload.chatId);
65
+ setChatId(connectedChatId);
66
+ setLinked(true);
67
+ setPendingLink(null);
68
+ localStorage.setItem(CHAT_ID_KEY, connectedChatId);
69
+ localStorage.setItem(LINKED_KEY, '1');
70
+ setStatus({
71
+ type: 'success',
72
+ text: 'Telegram connected. Send your first notification!',
73
+ });
74
+ } else if (payload.status === 'expired') {
75
+ setPendingLink(null);
76
+ setStatus({
77
+ type: 'error',
78
+ text: 'That connect link expired. Click Connect Telegram to try again.',
79
+ });
80
+ }
81
+ } catch {
82
+ // Transient network blip: keep polling until the link expires.
83
+ }
84
+ }, 2000);
85
+
86
+ return () => {
87
+ cancelled = true;
88
+ clearInterval(timer);
89
+ };
90
+ }, [pendingLink]);
91
+
92
+ async function handleConnect() {
93
+ setConnecting(true);
94
+ setStatus(null);
95
+
96
+ try {
97
+ const response = await fetch(`${API_BASE}/api/telegram/link`, { method: 'POST' });
98
+ const payload = await response.json().catch(() => null);
99
+
100
+ if (!response.ok) {
101
+ throw new Error(readErrorMessage(payload, 'Could not start the Telegram connection.'));
102
+ }
103
+
104
+ setPendingLink(payload);
105
+ if (payload.username) {
106
+ setBotInfo({ username: payload.username, deepLink: `https://t.me/${payload.username}` });
107
+ }
108
+
109
+ // Called from a click handler, so popup blockers normally allow this.
110
+ // We prefer the web.telegram.org URL (handles bot start payloads correctly
111
+ // inside Telegram Web) and fall back to the t.me link.
112
+ window.open(payload.webLink || payload.deepLink, '_blank', 'noopener,noreferrer');
113
+ } catch (error) {
114
+ setStatus({
115
+ type: 'error',
116
+ text:
117
+ error instanceof TypeError
118
+ ? `Could not reach the API at ${API_BASE}. Is the backend running?`
119
+ : error.message,
120
+ });
121
+ } finally {
122
+ setConnecting(false);
123
+ }
124
+ }
125
+
126
+ function handleDisconnect() {
127
+ setChatId('');
128
+ setLinked(false);
129
+ setPendingLink(null);
130
+ setStatus(null);
131
+ localStorage.removeItem(CHAT_ID_KEY);
132
+ localStorage.removeItem(LINKED_KEY);
133
+ }
134
+
135
+ function handleManualChatIdChange(event) {
136
+ setChatId(event.target.value);
137
+ if (linked) {
138
+ setLinked(false);
139
+ localStorage.removeItem(LINKED_KEY);
140
+ }
141
+ }
142
+
143
+ async function handleSubmit(event) {
144
+ event.preventDefault();
145
+
146
+ const trimmedChatId = chatId.trim();
147
+ const trimmedMessage = message.trim();
148
+
149
+ if (!trimmedChatId) {
150
+ setStatus({
151
+ type: 'error',
152
+ text: 'Connect Telegram first, or open "Use a Chat ID manually" below.',
153
+ });
154
+ return;
155
+ }
156
+
157
+ if (!/^-?\d+$/.test(trimmedChatId)) {
158
+ setStatus({ type: 'error', text: 'Enter a numeric chat ID, e.g. 123456789.' });
159
+ return;
160
+ }
161
+
162
+ if (!trimmedMessage) {
163
+ setStatus({ type: 'error', text: 'Write a message before sending.' });
164
+ return;
165
+ }
166
+
167
+ setSending(true);
168
+ setStatus({ type: 'pending', text: 'Sending…' });
169
+ localStorage.setItem(CHAT_ID_KEY, trimmedChatId);
170
+
171
+ try {
172
+ const response = await fetch(`${API_BASE}/api/telegram/send`, {
173
+ method: 'POST',
174
+ headers: { 'Content-Type': 'application/json' },
175
+ body: JSON.stringify({ chatId: trimmedChatId, message: trimmedMessage }),
176
+ });
177
+
178
+ const payload = await response.json().catch(() => null);
179
+
180
+ if (!response.ok) {
181
+ throw new Error(readErrorMessage(payload, `Request failed with status ${response.status}.`));
182
+ }
183
+
184
+ setStatus({
185
+ type: 'success',
186
+ text: `Notification sent to chat ${trimmedChatId}.`,
187
+ });
188
+ } catch (error) {
189
+ const isNetworkError = error instanceof TypeError;
190
+ setStatus({
191
+ type: 'error',
192
+ text: isNetworkError
193
+ ? `Could not reach the API at ${API_BASE}. Is the backend running?`
194
+ : error.message,
195
+ });
196
+ } finally {
197
+ setSending(false);
198
+ }
199
+ }
200
+
201
+ return (
202
+ <main className="page">
203
+ <section className="card" aria-labelledby="title">
204
+ <header className="card__header">
205
+ <div className="brand" aria-hidden="true">
206
+ ✈️
207
+ </div>
208
+ <div>
209
+ <h1 id="title">Telegram Notifier</h1>
210
+ <p className="subtitle">Connect Telegram, then send a message with one-tap actions.</p>
211
+ </div>
212
+ <span
213
+ className={`pill ${apiOnline === null ? 'pill--idle' : apiOnline ? 'pill--online' : 'pill--offline'}`}
214
+ title={`${API_BASE}/api/telegram/health`}
215
+ >
216
+ <span className="pill__dot" aria-hidden="true" />
217
+ {apiOnline === null ? 'Checking API' : apiOnline ? 'API online' : 'API offline'}
218
+ </span>
219
+ </header>
220
+
221
+ <form className="form" onSubmit={handleSubmit} noValidate>
222
+ <div className="field">
223
+ <span className="field__label">Recipient</span>
224
+
225
+ {linked ? (
226
+ <div className="connected">
227
+ <span className="connected__dot" aria-hidden="true" />
228
+ <div className="connected__text">
229
+ <strong>Telegram connected</strong>
230
+ <span className="hint">Chat ID {chatId}</span>
231
+ </div>
232
+ <button type="button" className="text-button" onClick={handleDisconnect}>
233
+ Disconnect
234
+ </button>
235
+ </div>
236
+ ) : pendingLink ? (
237
+ <div className="connect connect--pending">
238
+ <p className="connect__text">
239
+ <strong>Waiting for Telegram…</strong> Press <strong>Start</strong> in the chat
240
+ that just opened.
241
+ </p>
242
+ <div className="connect__actions">
243
+ <a
244
+ className="text-link"
245
+ href={pendingLink.webLink || pendingLink.deepLink}
246
+ target="_blank"
247
+ rel="noreferrer"
248
+ >
249
+ Open Telegram again
250
+ </a>
251
+ <button
252
+ type="button"
253
+ className="text-button"
254
+ onClick={() => setPendingLink(null)}
255
+ >
256
+ Cancel
257
+ </button>
258
+ </div>
259
+ </div>
260
+ ) : (
261
+ <div className="connect">
262
+ <button
263
+ type="button"
264
+ className="button button--secondary"
265
+ onClick={handleConnect}
266
+ disabled={connecting}
267
+ >
268
+ {connecting
269
+ ? 'Opening Telegram…'
270
+ : `Connect Telegram${botInfo?.username ? ` (@${botInfo.username})` : ''}`}
271
+ </button>
272
+ <p className="hint">
273
+ Opens {botInfo?.username ? `@${botInfo.username}` : 'our bot'} in Telegram. Press{' '}
274
+ <strong>Start</strong> once — your Chat ID is filled in automatically.
275
+ </p>
276
+ </div>
277
+ )}
278
+ </div>
279
+
280
+ <details className="manual">
281
+ <summary>Use a Chat ID manually</summary>
282
+ <div className="field manual__field">
283
+ <label htmlFor="chatId">Chat ID</label>
284
+ <input
285
+ id="chatId"
286
+ name="chatId"
287
+ type="text"
288
+ inputMode="numeric"
289
+ autoComplete="off"
290
+ placeholder="123456789"
291
+ value={chatId}
292
+ onChange={handleManualChatIdChange}
293
+ />
294
+ <p className="hint">
295
+ Message the bot with <strong>/start</strong> and it will reply with your Chat ID.
296
+ </p>
297
+ </div>
298
+ </details>
299
+
300
+ <div className="field">
301
+ <div className="label-row">
302
+ <label htmlFor="message">Notification Message</label>
303
+ <span
304
+ className={`counter ${message.length > MAX_MESSAGE_LENGTH ? 'counter--over' : ''}`}
305
+ >
306
+ {message.length}/{MAX_MESSAGE_LENGTH}
307
+ </span>
308
+ </div>
309
+ <textarea
310
+ id="message"
311
+ name="message"
312
+ rows={5}
313
+ maxLength={MAX_MESSAGE_LENGTH}
314
+ placeholder="Deploy finished — take a look when you can."
315
+ value={message}
316
+ onChange={(event) => setMessage(event.target.value)}
317
+ />
318
+ </div>
319
+
320
+ <button className="button" type="submit" disabled={sending}>
321
+ {sending ? 'Sending…' : 'Send Notification'}
322
+ </button>
323
+
324
+ <div className="status" role="status" aria-live="polite">
325
+ {status && (
326
+ <span className={`badge badge--${status.type}`}>
327
+ <span aria-hidden="true">
328
+ {status.type === 'success' ? '✓' : status.type === 'error' ? '!' : '…'}
329
+ </span>
330
+ {status.text}
331
+ </span>
332
+ )}
333
+ </div>
334
+ </form>
335
+
336
+ <footer className="card__footer">
337
+ <span className="preview-label">What your recipient will see</span>
338
+ <div className="bubble">
339
+ <p>{message.trim() || 'Your notification text'}</p>
340
+ <div className="keyboard" aria-hidden="true">
341
+ <span>Mark Done ✅</span>
342
+ <span>Snooze ⏰</span>
343
+ </div>
344
+ </div>
345
+ </footer>
346
+ </section>
347
+ </main>
348
+ );
349
+ }
projects/telegram-send-demo/frontend/src/main.jsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App.jsx';
4
+
5
+ createRoot(document.getElementById('root')).render(
6
+ <StrictMode>
7
+ <App />
8
+ </StrictMode>,
9
+ );
projects/telegram-send-demo/frontend/vite.config.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ },
9
+ });