mikrotik-telegram 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to `mikrotik-telegram` are documented here.
4
4
 
5
5
  The project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.2.0] - 2026-09-12
8
+
9
+ ### Added
10
+
11
+ - Complete `examples/production-bot` Node.js production example.
12
+ - Webhook server, operational command catalogue, alert loop, automation rules, and audit commands.
13
+
14
+ ### Fixed
15
+
16
+ - Published ESM declarations now use Node.js-compatible `.js` import specifiers.
17
+ - Package builds with a standalone NodeNext configuration for external consumers.
18
+
7
19
  ## [0.1.0] - 2026-09-12
8
20
 
9
21
  ### Added
package/README.md CHANGED
@@ -168,6 +168,10 @@ The library owns RouterOS and Telegram behavior but does not own your web framew
168
168
 
169
169
  The public entry point exports configuration, REST transport, services, command registry, runtime adapters, alerting, automation, protected actions, types, and audit helpers from `src/index.ts`.
170
170
 
171
+ ## Complete example
172
+
173
+ See [`examples/production-bot`](./examples/production-bot) for a complete Node.js 20 bot with webhook handling, role-based commands, vouchers, CRUD operations, protected mutations, alerts, automation, and audit export.
174
+
171
175
  ## Versioning
172
176
 
173
177
  This package follows [Semantic Versioning](https://semver.org/):
package/dist/alerts.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { HotspotProfile, SystemHealth, TrafficMetrics } from './types';
2
+ import type { HotspotProfile, SystemHealth, TrafficMetrics } from './types.js';
3
3
  export declare const AlertSeveritySchema: z.ZodEnum<{
4
4
  info: "info";
5
5
  warning: "warning";
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { Alert } from './alerts';
2
+ import type { Alert } from './alerts.js';
3
3
  export declare const AutomationActionSchema: z.ZodEnum<{
4
4
  "health-check": "health-check";
5
5
  "stock-check": "stock-check";
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- export * from './config';
2
- export * from './errors';
3
- export * from './rest-client';
4
- export * from './mikrotik-service';
5
- export * from './telegram-client';
6
- export * from './telegram-commands';
7
- export * from './runtime';
8
- export * from './runtime-adapters';
9
- export * from './alerts';
10
- export * from './automation';
11
- export * from './actions';
12
- export * from './types';
13
- export * from './audit';
1
+ export * from './config.js';
2
+ export * from './errors.js';
3
+ export * from './rest-client.js';
4
+ export * from './mikrotik-service.js';
5
+ export * from './telegram-client.js';
6
+ export * from './telegram-commands.js';
7
+ export * from './runtime.js';
8
+ export * from './runtime-adapters.js';
9
+ export * from './alerts.js';
10
+ export * from './automation.js';
11
+ export * from './actions.js';
12
+ export * from './types.js';
13
+ export * from './audit.js';
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
- export * from './config';
2
- export * from './errors';
3
- export * from './rest-client';
4
- export * from './mikrotik-service';
5
- export * from './telegram-client';
6
- export * from './telegram-commands';
7
- export * from './runtime';
8
- export * from './runtime-adapters';
9
- export * from './alerts';
10
- export * from './automation';
11
- export * from './actions';
12
- export * from './types';
13
- export * from './audit';
1
+ export * from './config.js';
2
+ export * from './errors.js';
3
+ export * from './rest-client.js';
4
+ export * from './mikrotik-service.js';
5
+ export * from './telegram-client.js';
6
+ export * from './telegram-commands.js';
7
+ export * from './runtime.js';
8
+ export * from './runtime-adapters.js';
9
+ export * from './alerts.js';
10
+ export * from './automation.js';
11
+ export * from './actions.js';
12
+ export * from './types.js';
13
+ export * from './audit.js';
@@ -1,5 +1,5 @@
1
- import { RouterOsRestClient } from './rest-client';
2
- import type { ActiveSession, CreateHotspotUserInput, CreateProfileInput, CreateSchedulerInput, CreateScriptInput, HotspotProfile, HotspotUser, MutationResult, RouterId, RouterScheduler, RouterScript, SystemHealth, UpdateHotspotUserInput, UpdateProfileInput, UpdateSchedulerInput, UserQuery, GenerateVouchersInput, GenerateVouchersResult, BanUserInput, BanResult, UnbanUserInput, DeleteUserInput, DisconnectUserInput } from './types';
1
+ import { RouterOsRestClient } from './rest-client.js';
2
+ import type { ActiveSession, CreateHotspotUserInput, CreateProfileInput, CreateSchedulerInput, CreateScriptInput, HotspotProfile, HotspotUser, MutationResult, RouterId, RouterScheduler, RouterScript, SystemHealth, UpdateHotspotUserInput, UpdateProfileInput, UpdateSchedulerInput, UserQuery, GenerateVouchersInput, GenerateVouchersResult, BanUserInput, BanResult, UnbanUserInput, DeleteUserInput, DisconnectUserInput } from './types.js';
3
3
  export declare class MikroTikService {
4
4
  private readonly client;
5
5
  constructor(client: RouterOsRestClient);
@@ -1,4 +1,4 @@
1
- import type { RouterPayload, RouterRecord, RouterResponse } from './types';
1
+ import type { RouterPayload, RouterRecord, RouterResponse } from './types.js';
2
2
  export interface RestClientOptions {
3
3
  readonly host: string;
4
4
  readonly port: number;
@@ -1,4 +1,4 @@
1
- import { RouterOsHttpError } from './errors';
1
+ import { RouterOsHttpError } from './errors.js';
2
2
  export class RouterOsRestClient {
3
3
  baseUrl;
4
4
  authorization;
@@ -1,4 +1,4 @@
1
- import type { TelegramUpdate } from './types';
1
+ import type { TelegramUpdate } from './types.js';
2
2
  export interface RuntimeAdapter {
3
3
  readonly fetch: typeof fetch;
4
4
  now(): Date;
package/dist/runtime.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { PackageConfig } from './config';
2
- import { MikroTikService } from './mikrotik-service';
3
- import { TelegramCommandRegistry } from './telegram-commands';
4
- import type { CommandResponse, TelegramUpdate } from './types';
1
+ import type { PackageConfig } from './config.js';
2
+ import { MikroTikService } from './mikrotik-service.js';
3
+ import { TelegramCommandRegistry } from './telegram-commands.js';
4
+ import type { CommandResponse, TelegramUpdate } from './types.js';
5
5
  export interface NetPulseRuntime {
6
6
  readonly registry: TelegramCommandRegistry;
7
7
  readonly mikrotik: MikroTikService;
package/dist/runtime.js CHANGED
@@ -1,8 +1,8 @@
1
- import { AuthorizationError } from './errors';
2
- import { RouterOsRestClient } from './rest-client';
3
- import { MikroTikService } from './mikrotik-service';
4
- import { TelegramClient } from './telegram-client';
5
- import { TelegramCommandRegistry } from './telegram-commands';
1
+ import { AuthorizationError } from './errors.js';
2
+ import { RouterOsRestClient } from './rest-client.js';
3
+ import { MikroTikService } from './mikrotik-service.js';
4
+ import { TelegramClient } from './telegram-client.js';
5
+ import { TelegramCommandRegistry } from './telegram-commands.js';
6
6
  function roleFor(config, chatId) {
7
7
  if (config.security.adminChatIds.includes(chatId))
8
8
  return 'admin';
@@ -1,4 +1,4 @@
1
- import type { CommandResponse } from './types';
1
+ import type { CommandResponse } from './types.js';
2
2
  export interface TelegramClientOptions {
3
3
  readonly token: string;
4
4
  readonly fetch?: typeof fetch;
@@ -1,4 +1,4 @@
1
- import type { CommandContext, CommandDefinition, CommandResponse } from './types';
1
+ import type { CommandContext, CommandDefinition, CommandResponse } from './types.js';
2
2
  export interface CommandRegistryOptions {
3
3
  readonly confirmationTtlMs?: number;
4
4
  readonly now?: () => number;
@@ -1,4 +1,4 @@
1
- import { AuthorizationError, ConfirmationRequiredError } from './errors';
1
+ import { AuthorizationError, ConfirmationRequiredError } from './errors.js';
2
2
  const ROLE_WEIGHT = { viewer: 1, operator: 2, admin: 3 };
3
3
  export function parseCommand(text) {
4
4
  const tokens = text.trim().split(/\s+/).filter(Boolean);
@@ -0,0 +1,16 @@
1
+ MIKROTIK_HOST=192.168.1.64
2
+ MIKROTIK_USER=netpulse-bot
3
+ MIKROTIK_PASSWORD=replace-me
4
+ MIKROTIK_HTTP_PORT=443
5
+ MIKROTIK_HTTPS=true
6
+ MIKROTIK_TIMEOUT_MS=10000
7
+
8
+ TELEGRAM_BOT_TOKEN=replace-me
9
+ TELEGRAM_ALLOWED_CHAT_IDS=123456789
10
+ TELEGRAM_ADMIN_CHAT_IDS=123456789
11
+ TELEGRAM_OPERATOR_CHAT_IDS=123456789
12
+ TELEGRAM_WEBHOOK_SECRET=use-at-least-16-characters
13
+ TELEGRAM_CONFIRMATION_TTL_SECONDS=60
14
+
15
+ BOT_PORT=8787
16
+ ALERT_INTERVAL_MS=300000
@@ -0,0 +1,48 @@
1
+ # Production bot example
2
+
3
+ A complete Node.js 20 example using the published `mikrotik-telegram` package.
4
+ It is deliberately independent from Next.js, the parent application database, and the legacy Telegram bot.
5
+
6
+ ## Features exercised
7
+
8
+ - Environment validation with `createConfigFromEnv()`.
9
+ - RouterOS REST health, users, profiles, sessions, vouchers, scripts, and schedulers.
10
+ - Telegram commands with viewer/operator/admin roles.
11
+ - Expiring confirmation flow for `/ban` and `/disconnect`.
12
+ - Protected action catalog and batch guardrails.
13
+ - Router, traffic, and profile-stock alert evaluation.
14
+ - Scheduled automation with health, stock, cleanup, and report rules.
15
+ - Audit logging with JSON and CSV export commands.
16
+ - Node HTTP webhook adapter with Telegram secret validation.
17
+
18
+ ## Run
19
+
20
+ ```bash
21
+ cp .env.example .env
22
+ # Fill in .env, then:
23
+ npm install
24
+ npm run build
25
+ npm start
26
+ ```
27
+
28
+ The webhook endpoint is `POST http://localhost:${BOT_PORT:-8787}/telegram/webhook`.
29
+ Configure Telegram with `setWebhook` and the same `TELEGRAM_WEBHOOK_SECRET`.
30
+
31
+ ## Command catalogue
32
+
33
+ - `/start` or `/help`: command list
34
+ - `/health`: RouterOS health
35
+ - `/users profile=guest-2h limit=20`: hotspot users
36
+ - `/profiles`: hotspot profiles
37
+ - `/sessions`: active sessions
38
+ - `/vouchers count=10 profile=guest-2h dryRun=true`: preview or create vouchers
39
+ - `/create-user name=alice password=secret profile=guest-2h`: create a user
40
+ - `/ban address=192.0.2.10`: protected ban confirmation
41
+ - `/disconnect session=*1`: protected session disconnect
42
+ - `/create-script name=backup source=/system backup save name=nightly`: create script
43
+ - `/create-scheduler name=nightly interval=1d onEvent=/system backup save name=nightly`: create scheduler
44
+ - `/alerts`: run anomaly checks immediately
45
+ - `/automation`: list configured rules
46
+ - `/audit format=csv`: export the in-memory audit log
47
+
48
+ The example intentionally keeps destructive actions explicit. In production, persist audit entries and automation state in your own durable store.
@@ -0,0 +1,590 @@
1
+ {
2
+ "name": "mikrotik-telegram-production-example",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "mikrotik-telegram-production-example",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "mikrotik-telegram": "file:../.."
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "^22.0.0",
15
+ "tsx": "^4.19.0",
16
+ "typescript": "^5.9.3"
17
+ },
18
+ "engines": {
19
+ "node": ">=20"
20
+ }
21
+ },
22
+ "../..": {
23
+ "name": "mikrotik-telegram",
24
+ "version": "0.2.0",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "zod": "^4.6.1"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5.9.3"
31
+ },
32
+ "engines": {
33
+ "node": ">=20"
34
+ }
35
+ },
36
+ "node_modules/@esbuild/aix-ppc64": {
37
+ "version": "0.28.2",
38
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
39
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
40
+ "cpu": [
41
+ "ppc64"
42
+ ],
43
+ "dev": true,
44
+ "license": "MIT",
45
+ "optional": true,
46
+ "os": [
47
+ "aix"
48
+ ],
49
+ "engines": {
50
+ "node": ">=18"
51
+ }
52
+ },
53
+ "node_modules/@esbuild/android-arm": {
54
+ "version": "0.28.2",
55
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
56
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
57
+ "cpu": [
58
+ "arm"
59
+ ],
60
+ "dev": true,
61
+ "license": "MIT",
62
+ "optional": true,
63
+ "os": [
64
+ "android"
65
+ ],
66
+ "engines": {
67
+ "node": ">=18"
68
+ }
69
+ },
70
+ "node_modules/@esbuild/android-arm64": {
71
+ "version": "0.28.2",
72
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
73
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
74
+ "cpu": [
75
+ "arm64"
76
+ ],
77
+ "dev": true,
78
+ "license": "MIT",
79
+ "optional": true,
80
+ "os": [
81
+ "android"
82
+ ],
83
+ "engines": {
84
+ "node": ">=18"
85
+ }
86
+ },
87
+ "node_modules/@esbuild/android-x64": {
88
+ "version": "0.28.2",
89
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
90
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
91
+ "cpu": [
92
+ "x64"
93
+ ],
94
+ "dev": true,
95
+ "license": "MIT",
96
+ "optional": true,
97
+ "os": [
98
+ "android"
99
+ ],
100
+ "engines": {
101
+ "node": ">=18"
102
+ }
103
+ },
104
+ "node_modules/@esbuild/darwin-arm64": {
105
+ "version": "0.28.2",
106
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
107
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
108
+ "cpu": [
109
+ "arm64"
110
+ ],
111
+ "dev": true,
112
+ "license": "MIT",
113
+ "optional": true,
114
+ "os": [
115
+ "darwin"
116
+ ],
117
+ "engines": {
118
+ "node": ">=18"
119
+ }
120
+ },
121
+ "node_modules/@esbuild/darwin-x64": {
122
+ "version": "0.28.2",
123
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
124
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
125
+ "cpu": [
126
+ "x64"
127
+ ],
128
+ "dev": true,
129
+ "license": "MIT",
130
+ "optional": true,
131
+ "os": [
132
+ "darwin"
133
+ ],
134
+ "engines": {
135
+ "node": ">=18"
136
+ }
137
+ },
138
+ "node_modules/@esbuild/freebsd-arm64": {
139
+ "version": "0.28.2",
140
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
141
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
142
+ "cpu": [
143
+ "arm64"
144
+ ],
145
+ "dev": true,
146
+ "license": "MIT",
147
+ "optional": true,
148
+ "os": [
149
+ "freebsd"
150
+ ],
151
+ "engines": {
152
+ "node": ">=18"
153
+ }
154
+ },
155
+ "node_modules/@esbuild/freebsd-x64": {
156
+ "version": "0.28.2",
157
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
158
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
159
+ "cpu": [
160
+ "x64"
161
+ ],
162
+ "dev": true,
163
+ "license": "MIT",
164
+ "optional": true,
165
+ "os": [
166
+ "freebsd"
167
+ ],
168
+ "engines": {
169
+ "node": ">=18"
170
+ }
171
+ },
172
+ "node_modules/@esbuild/linux-arm": {
173
+ "version": "0.28.2",
174
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
175
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
176
+ "cpu": [
177
+ "arm"
178
+ ],
179
+ "dev": true,
180
+ "license": "MIT",
181
+ "optional": true,
182
+ "os": [
183
+ "linux"
184
+ ],
185
+ "engines": {
186
+ "node": ">=18"
187
+ }
188
+ },
189
+ "node_modules/@esbuild/linux-arm64": {
190
+ "version": "0.28.2",
191
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
192
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
193
+ "cpu": [
194
+ "arm64"
195
+ ],
196
+ "dev": true,
197
+ "license": "MIT",
198
+ "optional": true,
199
+ "os": [
200
+ "linux"
201
+ ],
202
+ "engines": {
203
+ "node": ">=18"
204
+ }
205
+ },
206
+ "node_modules/@esbuild/linux-ia32": {
207
+ "version": "0.28.2",
208
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
209
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
210
+ "cpu": [
211
+ "ia32"
212
+ ],
213
+ "dev": true,
214
+ "license": "MIT",
215
+ "optional": true,
216
+ "os": [
217
+ "linux"
218
+ ],
219
+ "engines": {
220
+ "node": ">=18"
221
+ }
222
+ },
223
+ "node_modules/@esbuild/linux-loong64": {
224
+ "version": "0.28.2",
225
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
226
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
227
+ "cpu": [
228
+ "loong64"
229
+ ],
230
+ "dev": true,
231
+ "license": "MIT",
232
+ "optional": true,
233
+ "os": [
234
+ "linux"
235
+ ],
236
+ "engines": {
237
+ "node": ">=18"
238
+ }
239
+ },
240
+ "node_modules/@esbuild/linux-mips64el": {
241
+ "version": "0.28.2",
242
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
243
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
244
+ "cpu": [
245
+ "mips64el"
246
+ ],
247
+ "dev": true,
248
+ "license": "MIT",
249
+ "optional": true,
250
+ "os": [
251
+ "linux"
252
+ ],
253
+ "engines": {
254
+ "node": ">=18"
255
+ }
256
+ },
257
+ "node_modules/@esbuild/linux-ppc64": {
258
+ "version": "0.28.2",
259
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
260
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
261
+ "cpu": [
262
+ "ppc64"
263
+ ],
264
+ "dev": true,
265
+ "license": "MIT",
266
+ "optional": true,
267
+ "os": [
268
+ "linux"
269
+ ],
270
+ "engines": {
271
+ "node": ">=18"
272
+ }
273
+ },
274
+ "node_modules/@esbuild/linux-riscv64": {
275
+ "version": "0.28.2",
276
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
277
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
278
+ "cpu": [
279
+ "riscv64"
280
+ ],
281
+ "dev": true,
282
+ "license": "MIT",
283
+ "optional": true,
284
+ "os": [
285
+ "linux"
286
+ ],
287
+ "engines": {
288
+ "node": ">=18"
289
+ }
290
+ },
291
+ "node_modules/@esbuild/linux-s390x": {
292
+ "version": "0.28.2",
293
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
294
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
295
+ "cpu": [
296
+ "s390x"
297
+ ],
298
+ "dev": true,
299
+ "license": "MIT",
300
+ "optional": true,
301
+ "os": [
302
+ "linux"
303
+ ],
304
+ "engines": {
305
+ "node": ">=18"
306
+ }
307
+ },
308
+ "node_modules/@esbuild/linux-x64": {
309
+ "version": "0.28.2",
310
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
311
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
312
+ "cpu": [
313
+ "x64"
314
+ ],
315
+ "dev": true,
316
+ "license": "MIT",
317
+ "optional": true,
318
+ "os": [
319
+ "linux"
320
+ ],
321
+ "engines": {
322
+ "node": ">=18"
323
+ }
324
+ },
325
+ "node_modules/@esbuild/netbsd-arm64": {
326
+ "version": "0.28.2",
327
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
328
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
329
+ "cpu": [
330
+ "arm64"
331
+ ],
332
+ "dev": true,
333
+ "license": "MIT",
334
+ "optional": true,
335
+ "os": [
336
+ "netbsd"
337
+ ],
338
+ "engines": {
339
+ "node": ">=18"
340
+ }
341
+ },
342
+ "node_modules/@esbuild/netbsd-x64": {
343
+ "version": "0.28.2",
344
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
345
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
346
+ "cpu": [
347
+ "x64"
348
+ ],
349
+ "dev": true,
350
+ "license": "MIT",
351
+ "optional": true,
352
+ "os": [
353
+ "netbsd"
354
+ ],
355
+ "engines": {
356
+ "node": ">=18"
357
+ }
358
+ },
359
+ "node_modules/@esbuild/openbsd-arm64": {
360
+ "version": "0.28.2",
361
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
362
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
363
+ "cpu": [
364
+ "arm64"
365
+ ],
366
+ "dev": true,
367
+ "license": "MIT",
368
+ "optional": true,
369
+ "os": [
370
+ "openbsd"
371
+ ],
372
+ "engines": {
373
+ "node": ">=18"
374
+ }
375
+ },
376
+ "node_modules/@esbuild/openbsd-x64": {
377
+ "version": "0.28.2",
378
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
379
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
380
+ "cpu": [
381
+ "x64"
382
+ ],
383
+ "dev": true,
384
+ "license": "MIT",
385
+ "optional": true,
386
+ "os": [
387
+ "openbsd"
388
+ ],
389
+ "engines": {
390
+ "node": ">=18"
391
+ }
392
+ },
393
+ "node_modules/@esbuild/openharmony-arm64": {
394
+ "version": "0.28.2",
395
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
396
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
397
+ "cpu": [
398
+ "arm64"
399
+ ],
400
+ "dev": true,
401
+ "license": "MIT",
402
+ "optional": true,
403
+ "os": [
404
+ "openharmony"
405
+ ],
406
+ "engines": {
407
+ "node": ">=18"
408
+ }
409
+ },
410
+ "node_modules/@esbuild/sunos-x64": {
411
+ "version": "0.28.2",
412
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
413
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
414
+ "cpu": [
415
+ "x64"
416
+ ],
417
+ "dev": true,
418
+ "license": "MIT",
419
+ "optional": true,
420
+ "os": [
421
+ "sunos"
422
+ ],
423
+ "engines": {
424
+ "node": ">=18"
425
+ }
426
+ },
427
+ "node_modules/@esbuild/win32-arm64": {
428
+ "version": "0.28.2",
429
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
430
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
431
+ "cpu": [
432
+ "arm64"
433
+ ],
434
+ "dev": true,
435
+ "license": "MIT",
436
+ "optional": true,
437
+ "os": [
438
+ "win32"
439
+ ],
440
+ "engines": {
441
+ "node": ">=18"
442
+ }
443
+ },
444
+ "node_modules/@esbuild/win32-ia32": {
445
+ "version": "0.28.2",
446
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
447
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
448
+ "cpu": [
449
+ "ia32"
450
+ ],
451
+ "dev": true,
452
+ "license": "MIT",
453
+ "optional": true,
454
+ "os": [
455
+ "win32"
456
+ ],
457
+ "engines": {
458
+ "node": ">=18"
459
+ }
460
+ },
461
+ "node_modules/@esbuild/win32-x64": {
462
+ "version": "0.28.2",
463
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
464
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
465
+ "cpu": [
466
+ "x64"
467
+ ],
468
+ "dev": true,
469
+ "license": "MIT",
470
+ "optional": true,
471
+ "os": [
472
+ "win32"
473
+ ],
474
+ "engines": {
475
+ "node": ">=18"
476
+ }
477
+ },
478
+ "node_modules/@types/node": {
479
+ "version": "22.20.2",
480
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz",
481
+ "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==",
482
+ "dev": true,
483
+ "license": "MIT",
484
+ "dependencies": {
485
+ "undici-types": "~6.21.0"
486
+ }
487
+ },
488
+ "node_modules/esbuild": {
489
+ "version": "0.28.2",
490
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
491
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
492
+ "dev": true,
493
+ "hasInstallScript": true,
494
+ "license": "MIT",
495
+ "bin": {
496
+ "esbuild": "bin/esbuild"
497
+ },
498
+ "engines": {
499
+ "node": ">=18"
500
+ },
501
+ "optionalDependencies": {
502
+ "@esbuild/aix-ppc64": "0.28.2",
503
+ "@esbuild/android-arm": "0.28.2",
504
+ "@esbuild/android-arm64": "0.28.2",
505
+ "@esbuild/android-x64": "0.28.2",
506
+ "@esbuild/darwin-arm64": "0.28.2",
507
+ "@esbuild/darwin-x64": "0.28.2",
508
+ "@esbuild/freebsd-arm64": "0.28.2",
509
+ "@esbuild/freebsd-x64": "0.28.2",
510
+ "@esbuild/linux-arm": "0.28.2",
511
+ "@esbuild/linux-arm64": "0.28.2",
512
+ "@esbuild/linux-ia32": "0.28.2",
513
+ "@esbuild/linux-loong64": "0.28.2",
514
+ "@esbuild/linux-mips64el": "0.28.2",
515
+ "@esbuild/linux-ppc64": "0.28.2",
516
+ "@esbuild/linux-riscv64": "0.28.2",
517
+ "@esbuild/linux-s390x": "0.28.2",
518
+ "@esbuild/linux-x64": "0.28.2",
519
+ "@esbuild/netbsd-arm64": "0.28.2",
520
+ "@esbuild/netbsd-x64": "0.28.2",
521
+ "@esbuild/openbsd-arm64": "0.28.2",
522
+ "@esbuild/openbsd-x64": "0.28.2",
523
+ "@esbuild/openharmony-arm64": "0.28.2",
524
+ "@esbuild/sunos-x64": "0.28.2",
525
+ "@esbuild/win32-arm64": "0.28.2",
526
+ "@esbuild/win32-ia32": "0.28.2",
527
+ "@esbuild/win32-x64": "0.28.2"
528
+ }
529
+ },
530
+ "node_modules/fsevents": {
531
+ "version": "2.3.3",
532
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
533
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
534
+ "dev": true,
535
+ "hasInstallScript": true,
536
+ "license": "MIT",
537
+ "optional": true,
538
+ "os": [
539
+ "darwin"
540
+ ],
541
+ "engines": {
542
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
543
+ }
544
+ },
545
+ "node_modules/mikrotik-telegram": {
546
+ "resolved": "../..",
547
+ "link": true
548
+ },
549
+ "node_modules/tsx": {
550
+ "version": "4.23.13",
551
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz",
552
+ "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
553
+ "dev": true,
554
+ "license": "MIT",
555
+ "dependencies": {
556
+ "esbuild": "~0.28.0"
557
+ },
558
+ "bin": {
559
+ "tsx": "dist/cli.mjs"
560
+ },
561
+ "engines": {
562
+ "node": ">=18.0.0"
563
+ },
564
+ "optionalDependencies": {
565
+ "fsevents": "~2.3.3"
566
+ }
567
+ },
568
+ "node_modules/typescript": {
569
+ "version": "5.9.3",
570
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
571
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
572
+ "dev": true,
573
+ "license": "Apache-2.0",
574
+ "bin": {
575
+ "tsc": "bin/tsc",
576
+ "tsserver": "bin/tsserver"
577
+ },
578
+ "engines": {
579
+ "node": ">=14.17"
580
+ }
581
+ },
582
+ "node_modules/undici-types": {
583
+ "version": "6.21.0",
584
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
585
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
586
+ "dev": true,
587
+ "license": "MIT"
588
+ }
589
+ }
590
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "mikrotik-telegram-production-example",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.json",
11
+ "start": "node dist/index.js",
12
+ "dev": "tsx src/index.ts"
13
+ },
14
+ "dependencies": {
15
+ "mikrotik-telegram": "file:../.."
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.0.0",
19
+ "tsx": "^4.19.0",
20
+ "typescript": "^5.9.3"
21
+ }
22
+ }
@@ -0,0 +1,194 @@
1
+ import { createServer, type IncomingMessage } from 'node:http';
2
+ import {
3
+ AlertEngine,
4
+ AutomationEngine,
5
+ createAuditLogger,
6
+ createConfigFromEnv,
7
+ createProtectedAction,
8
+ createRouterActionCatalog,
9
+ createRuntime,
10
+ createWebhookHandler,
11
+ evaluateRouterAnomalies,
12
+ evaluateStockAnomalies,
13
+ evaluateTrafficAnomalies,
14
+ exportAuditLog,
15
+ TelegramClient,
16
+ type ActionContext,
17
+ type Alert,
18
+ type AutomationAction,
19
+ type CommandContext,
20
+ type CommandResponse,
21
+ type TelegramUpdate,
22
+ } from 'mikrotik-telegram';
23
+
24
+ const config = createConfigFromEnv(process.env);
25
+ const runtime = createRuntime(config);
26
+ const telegram = new TelegramClient({ token: config.telegram.token });
27
+ const audit = createAuditLogger('production-bot');
28
+ const alertEngine = new AlertEngine(15 * 60_000);
29
+ const automation = new AutomationEngine();
30
+
31
+ function response(text: string): CommandResponse {
32
+ return { text };
33
+ }
34
+
35
+ function formatAlerts(alerts: readonly Alert[]): string {
36
+ return alerts.length
37
+ ? alerts.map((alert) => `${alert.severity.toUpperCase()}: ${alert.title}\n${alert.message}`).join('\n\n')
38
+ : 'Aucune anomalie détectée.';
39
+ }
40
+
41
+ async function notifyOperators(alerts: readonly Alert[]): Promise<void> {
42
+ if (!alerts.length) return;
43
+ const message = formatAlerts(alerts);
44
+ for (const chatId of config.security.operatorChatIds) {
45
+ await telegram.sendMessage(chatId, response(`🚨 NetPulse alertes\n\n${message}`));
46
+ }
47
+ }
48
+
49
+ function actionContext(context: CommandContext): ActionContext {
50
+ return { chatId: context.chatId, userId: context.userId, role: context.role };
51
+ }
52
+
53
+ const actions = createRouterActionCatalog({
54
+ async health() {
55
+ return { ok: true, data: await runtime.mikrotik.getSystemHealth() };
56
+ },
57
+ async disconnectUser(input) {
58
+ return { ok: true, data: await runtime.mikrotik.disconnectUser(input) };
59
+ },
60
+ async banUser(input) {
61
+ return { ok: true, data: await runtime.mikrotik.banUser(input) };
62
+ },
63
+ });
64
+
65
+ const disconnectAction = createProtectedAction(
66
+ 'disconnect-session',
67
+ async (input: { sessionId: string }, context) => {
68
+ const result = await actions.disconnectUser(input);
69
+ audit.log({ action: 'session.disconnect', actor: context.userId, resource: input.sessionId, status: result.ok ? 'success' : 'failure', metadata: { result: result.data } });
70
+ return result;
71
+ },
72
+ { requiredRole: 'operator', requiresConfirmation: true, maxBatchSize: 1, confirm: async () => ({ ok: true }) },
73
+ );
74
+
75
+ const banAction = createProtectedAction(
76
+ 'ban-user',
77
+ async (input: { address: string; timeout?: string }, context) => {
78
+ const result = await actions.banUser(input);
79
+ audit.log({ action: 'user.ban', actor: context.userId, resource: input.address, status: result.ok ? 'success' : 'failure', metadata: { result: result.data } });
80
+ return result;
81
+ },
82
+ { requiredRole: 'admin', requiresConfirmation: true, confirm: async () => ({ ok: true }) },
83
+ );
84
+
85
+ async function runHealthCheck(): Promise<{ alerts: readonly Alert[] }> {
86
+ try {
87
+ const health = await runtime.mikrotik.getSystemHealth();
88
+ const evaluation = evaluateRouterAnomalies({ online: true, health });
89
+ const filtered = alertEngine.filter(evaluation.alerts);
90
+ audit.log({ action: 'automation.health-check', resource: health.identity, status: 'success', metadata: { alerts: filtered.alerts.length } });
91
+ await notifyOperators(filtered.alerts);
92
+ return filtered;
93
+ } catch (error) {
94
+ const evaluation = evaluateRouterAnomalies({ online: false });
95
+ const filtered = alertEngine.filter(evaluation.alerts);
96
+ audit.log({ action: 'automation.health-check', status: 'failure', metadata: { error: String(error) } });
97
+ await notifyOperators(filtered.alerts);
98
+ return filtered;
99
+ }
100
+ }
101
+
102
+ async function runStockCheck(): Promise<{ alerts: readonly Alert[] }> {
103
+ const profiles = await runtime.mikrotik.listProfiles();
104
+ const filtered = alertEngine.filter(evaluateStockAnomalies(profiles).alerts);
105
+ audit.log({ action: 'automation.stock-check', status: 'success', metadata: { profiles: profiles.length, alerts: filtered.alerts.length } });
106
+ await notifyOperators(filtered.alerts);
107
+ return filtered;
108
+ }
109
+
110
+ async function executeAutomation(action: AutomationAction, rule: { id: string }) {
111
+ const startedAt = new Date().toISOString();
112
+ if (action === 'health-check') {
113
+ const result = await runHealthCheck();
114
+ return { ruleId: rule.id, action, success: true, startedAt, finishedAt: new Date().toISOString(), alerts: result.alerts };
115
+ }
116
+ if (action === 'stock-check') {
117
+ const result = await runStockCheck();
118
+ return { ruleId: rule.id, action, success: true, startedAt, finishedAt: new Date().toISOString(), alerts: result.alerts };
119
+ }
120
+ if (action === 'cleanup-sessions') {
121
+ const sessions = await runtime.mikrotik.listActiveSessions();
122
+ audit.log({ action: 'automation.cleanup-sessions', status: 'success', metadata: { activeSessions: sessions.length } });
123
+ return { ruleId: rule.id, action, success: true, startedAt, finishedAt: new Date().toISOString() };
124
+ }
125
+ audit.log({ action: `automation.${action}`, status: 'success' });
126
+ return { ruleId: rule.id, action, success: true, startedAt, finishedAt: new Date().toISOString() };
127
+ }
128
+
129
+ automation.register({ id: 'health', name: 'Router health', action: 'health-check', intervalMs: Number(process.env.ALERT_INTERVAL_MS || 300_000), enabled: true, runImmediately: true });
130
+ automation.register({ id: 'stock', name: 'Profile capacity', action: 'stock-check', intervalMs: 900_000, enabled: true, runImmediately: false });
131
+ automation.register({ id: 'sessions', name: 'Session inventory', action: 'cleanup-sessions', intervalMs: 600_000, enabled: true, runImmediately: false });
132
+
133
+ runtime.registry.registerMany([
134
+ { name: 'automation', description: 'Lister les automatisations', minimumRole: 'viewer', execute: async () => response(automation.list().map((rule) => `• ${rule.id}: ${rule.action} (${rule.enabled ? 'active' : 'pause'})`).join('\n')) },
135
+ { name: 'health', aliases: ['status'], description: 'État du routeur', minimumRole: 'viewer', execute: async () => { const health = await runtime.mikrotik.getSystemHealth(); return response(`🟢 ${health.identity} · ${health.model}\nCPU: ${health.cpuLoadPercent}%\nRAM libre: ${Math.round(health.freeMemoryBytes / 1048576)} MB\nREST: ${health.latencyMs} ms`); } },
136
+ { name: 'users', description: 'Lister les utilisateurs hotspot', minimumRole: 'viewer', execute: async (context) => { const users = await runtime.mikrotik.listHotspotUsers({ profile: context.args.profile, limit: Number(context.args.limit || 20) }); return response(users.length ? users.map((user) => `• ${user.name} · ${user.profile || 'default'} · ${user.id}`).join('\n') : 'Aucun utilisateur.'); } },
137
+ { name: 'profiles', description: 'Lister les profils', minimumRole: 'viewer', execute: async () => { const profiles = await runtime.mikrotik.listProfiles(); return response(profiles.map((profile) => `• ${profile.name} · ${profile.rateLimit || 'illimité'} · shared=${profile.sharedUsers ?? 0}`).join('\n') || 'Aucun profil.'); } },
138
+ { name: 'sessions', description: 'Lister les sessions', minimumRole: 'viewer', execute: async () => { const sessions = await runtime.mikrotik.listActiveSessions(); return response(sessions.map((session) => `• ${session.user} · ${session.address || '?'} · ${session.id}`).join('\n') || 'Aucune session.'); } },
139
+ { name: 'vouchers', aliases: ['generate'], description: 'Générer des vouchers', minimumRole: 'operator', execute: async (context) => { const result = await runtime.mikrotik.generateVouchers({ count: Number(context.args.count || context.positional[0] || 1), profile: context.args.profile || 'default', prefix: context.args.prefix || 'NET', limitUptime: context.args.duration, comment: context.args.comment, dryRun: context.args.dryRun === 'true' }); audit.log({ action: 'voucher.generate', actor: context.userId, status: 'success', metadata: { requested: result.requested, created: result.created, dryRun: result.dryRun } }); return response(`${result.dryRun ? 'Aperçu' : 'Créés'}: ${result.created || result.requested}\n${result.vouchers.map((voucher) => `${voucher.name} · ${voucher.password}`).join('\n')}`); } },
140
+ { name: 'create-user', description: 'Créer un utilisateur', minimumRole: 'operator', execute: async (context) => { const name = context.args.name || context.positional[0]; const password = context.args.password; const profile = context.args.profile || 'default'; if (!name || !password) return response('Usage: /create-user name=alice password=secret profile=guest-2h'); const user = await runtime.mikrotik.createHotspotUser({ name, password, profile, comment: context.args.comment }); audit.log({ action: 'user.create', actor: context.userId, resource: user.id, status: 'success', metadata: { name, profile } }); return response(`Utilisateur créé: ${user.name} · ${user.id}`); } },
141
+ { name: 'ban', description: 'Bannir une adresse', minimumRole: 'admin', execute: async (context) => { const address = context.args.address || context.positional[0]; if (!address) return response('Usage: /ban address=192.0.2.10'); if (context.args.confirm !== 'yes') return runtime.registry.requestConfirmation(context, `/ban address=${address} confirm=yes`); const result = await banAction({ address, timeout: context.args.timeout }, actionContext(context)); return response(result.ok ? `Adresse bannie: ${address}` : result.error || 'Échec du bannissement.'); } },
142
+ { name: 'disconnect', description: 'Déconnecter une session', minimumRole: 'operator', execute: async (context) => { const sessionId = context.args.session || context.positional[0]; if (!sessionId) return response('Usage: /disconnect session=*1'); if (context.args.confirm !== 'yes') return runtime.registry.requestConfirmation(context, `/disconnect session=${sessionId} confirm=yes`); const result = await disconnectAction({ sessionId }, actionContext(context)); return response(result.ok ? `Session déconnectée: ${sessionId}` : result.error || 'Échec de la déconnexion.'); } },
143
+ { name: 'create-script', description: 'Créer un script RouterOS', minimumRole: 'admin', execute: async (context) => { const name = context.args.name; const source = context.args.source || context.positional.slice(1).join(' '); if (!name || !source) return response('Usage: /create-script name=backup source=/system backup save'); const script = await runtime.mikrotik.createScript({ name, source }); audit.log({ action: 'script.create', actor: context.userId, resource: script.id, status: 'success' }); return response(`Script créé: ${script.name} · ${script.id}`); } },
144
+ { name: 'create-scheduler', description: 'Créer un scheduler RouterOS', minimumRole: 'admin', execute: async (context) => { const name = context.args.name; const interval = context.args.interval; const onEvent = context.args.onEvent; if (!name || !interval || !onEvent) return response('Usage: /create-scheduler name=nightly interval=1d onEvent=/system backup save'); const scheduler = await runtime.mikrotik.createScheduler({ name, interval, onEvent }); audit.log({ action: 'scheduler.create', actor: context.userId, resource: scheduler.id, status: 'success' }); return response(`Scheduler créé: ${scheduler.name} · ${scheduler.id}`); } },
145
+ { name: 'alerts', description: 'Lancer les contrôles', minimumRole: 'operator', execute: async () => { const health = await runHealthCheck(); const stock = await runStockCheck(); const traffic = evaluateTrafficAnomalies({ online: true, totalSessions: (await runtime.mikrotik.listActiveSessions()).length, peakSessions: 100 }); const filtered = alertEngine.filter(traffic.alerts); await notifyOperators(filtered.alerts); return response(formatAlerts([...health.alerts, ...stock.alerts, ...filtered.alerts])); } },
146
+ { name: 'audit', description: 'Exporter les audits', minimumRole: 'admin', execute: async (context) => { const format = context.args.format === 'csv' ? 'csv' : 'json'; return response(exportAuditLog(audit.snapshot(), format)); } },
147
+ ]);
148
+
149
+ async function handleUpdate(update: TelegramUpdate): Promise<unknown> {
150
+ if (update.callback_query?.data && update.callback_query.message) {
151
+ const chatId = String(update.callback_query.message.chat.id);
152
+ const callbackResponse = await runtime.registry.executeCallback(update.callback_query.data, chatId);
153
+ await telegram.answerCallbackQuery(update.callback_query.id, callbackResponse.text);
154
+ const confirmedCommand = callbackResponse.text.match(/^Action confirmée: (.+)$/)?.[1];
155
+ if (confirmedCommand) return runtime.handleUpdate({ update_id: update.update_id, message: { message_id: update.update_id, text: confirmedCommand, chat: { id: chatId, type: 'private' }, from: { id: update.callback_query.from.id } } });
156
+ return telegram.sendMessage(chatId, callbackResponse);
157
+ }
158
+ return runtime.handleUpdate(update);
159
+ }
160
+
161
+ function toRequest(request: IncomingMessage, body: string): Request {
162
+ const protocol = 'http';
163
+ const host = request.headers.host || 'localhost';
164
+ return new Request(`${protocol}://${host}${request.url || '/'}`, { method: request.method, headers: request.headers as Record<string, string>, body: request.method === 'POST' ? body : undefined });
165
+ }
166
+
167
+ const webhook = createWebhookHandler(handleUpdate, { secret: config.telegram.webhookSecret });
168
+ const server = createServer(async (request, response) => {
169
+ const chunks: Buffer[] = [];
170
+ request.on('data', (chunk: Buffer) => chunks.push(chunk));
171
+ request.on('end', async () => {
172
+ if (request.url !== '/telegram/webhook') { response.writeHead(404).end('Not Found'); return; }
173
+ const result = await webhook(toRequest(request, Buffer.concat(chunks).toString('utf8')));
174
+ response.writeHead(result.status, Object.fromEntries(result.headers.entries())).end(await result.text());
175
+ });
176
+ });
177
+
178
+ const port = Number(process.env.BOT_PORT || 8787);
179
+ server.listen(port, () => console.log(`Production bot listening on http://localhost:${port}/telegram/webhook`));
180
+
181
+ const automationTimer = setInterval(async () => {
182
+ const results = await automation.runDue({ now: new Date(), execute: executeAutomation });
183
+ for (const result of results) if (!result.success) audit.log({ action: `automation.${result.action}`, status: 'failure', metadata: { result } });
184
+ }, 30_000);
185
+
186
+ automationTimer.unref();
187
+
188
+ function shutdown(signal: string): void {
189
+ clearInterval(automationTimer);
190
+ server.close(() => { console.log(`Stopped on ${signal}`); process.exit(0); });
191
+ }
192
+
193
+ process.once('SIGINT', () => shutdown('SIGINT'));
194
+ process.once('SIGTERM', () => shutdown('SIGTERM'));
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "outDir": "dist",
12
+ "rootDir": "src",
13
+ "types": ["node"]
14
+ },
15
+ "include": ["src/**/*.ts"]
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikrotik-telegram",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Type-safe RouterOS REST automation and Telegram bot package for modern MikroTik operations.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -23,7 +23,8 @@
23
23
  "dist",
24
24
  "README.md",
25
25
  "LICENSE",
26
- "CHANGELOG.md"
26
+ "CHANGELOG.md",
27
+ "examples"
27
28
  ],
28
29
  "publishConfig": {
29
30
  "access": "public"