mikrotik-telegram 0.1.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 +16 -0
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/dist/actions.d.ts +38 -0
- package/dist/actions.js +40 -0
- package/dist/alerts.d.ts +50 -0
- package/dist/alerts.js +103 -0
- package/dist/audit.d.ts +20 -0
- package/dist/audit.js +44 -0
- package/dist/automation.d.ts +57 -0
- package/dist/automation.js +70 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +64 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +34 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/mikrotik-service.d.ts +27 -0
- package/dist/mikrotik-service.js +225 -0
- package/dist/rest-client.d.ts +24 -0
- package/dist/rest-client.js +55 -0
- package/dist/runtime-adapters.d.ts +56 -0
- package/dist/runtime-adapters.js +87 -0
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.js +46 -0
- package/dist/telegram-client.d.ts +19 -0
- package/dist/telegram-client.js +29 -0
- package/dist/telegram-commands.d.ts +26 -0
- package/dist/telegram-commands.js +76 -0
- package/dist/types.d.ts +209 -0
- package/dist/types.js +1 -0
- package/package.json +61 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `mikrotik-telegram` are documented here.
|
|
4
|
+
|
|
5
|
+
The project follows [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-09-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- RouterOS REST transport with typed CRUD helpers.
|
|
12
|
+
- MikroTik hotspot, profile, session, script, scheduler, and voucher services.
|
|
13
|
+
- Telegram command registry with role checks and expiring confirmations.
|
|
14
|
+
- Runtime adapters for Node.js, Bun, webhooks, workers, serverless, cron, and polling.
|
|
15
|
+
- Alert, automation, protected action, and audit logging utilities.
|
|
16
|
+
- Zod-validated environment configuration.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Netpulse
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# mikrotik-telegram
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/mikrotik-telegram)
|
|
4
|
+
[](https://www.npmjs.com/package/mikrotik-telegram)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
Typed, runtime-neutral automation for MikroTik RouterOS REST and Telegram operations.
|
|
9
|
+
|
|
10
|
+
This package is designed as an independent library. It does not import Next.js, React, database code, application routes, the legacy RouterOS socket client, or the legacy Telegram bot. Install it from npm in any compatible Node.js, Bun, worker, serverless, or webhook project.
|
|
11
|
+
|
|
12
|
+
## What it includes
|
|
13
|
+
|
|
14
|
+
- REST transport with typed CRUD helpers and timeout handling.
|
|
15
|
+
- Hotspot users, profiles, active sessions, scripts, and schedulers.
|
|
16
|
+
- Voucher batches with validation, duplicate checks, and dry-run mode.
|
|
17
|
+
- Telegram commands with aliases, role checks, and expiring confirmations.
|
|
18
|
+
- Webhook, polling, worker, cron, Node.js, and Bun runtime adapters.
|
|
19
|
+
- Router, traffic, and stock anomaly evaluation with cooldown/escalation.
|
|
20
|
+
- Protected actions with role and batch-size guardrails.
|
|
21
|
+
- JSON/CSV audit export for operational traceability.
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Node.js `>=20`, Bun, or a runtime providing `fetch`, `Request`, and `Response`.
|
|
26
|
+
- MikroTik RouterOS with REST enabled.
|
|
27
|
+
- Zod is installed automatically as a production dependency.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install mikrotik-telegram
|
|
33
|
+
# or
|
|
34
|
+
pnpm add mikrotik-telegram
|
|
35
|
+
# or
|
|
36
|
+
bun add mikrotik-telegram
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { createConfigFromEnv, createRuntime } from 'mikrotik-telegram';
|
|
43
|
+
|
|
44
|
+
const config = createConfigFromEnv(process.env);
|
|
45
|
+
const runtime = createRuntime(config);
|
|
46
|
+
|
|
47
|
+
const response = await runtime.handleUpdate({
|
|
48
|
+
update_id: 1,
|
|
49
|
+
message: {
|
|
50
|
+
message_id: 1,
|
|
51
|
+
text: '/status',
|
|
52
|
+
chat: { id: '123456789', type: 'private' },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log(response?.text);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Environment configuration
|
|
60
|
+
|
|
61
|
+
```env
|
|
62
|
+
MIKROTIK_HOST=192.168.1.64
|
|
63
|
+
MIKROTIK_USER=netpulse-bot
|
|
64
|
+
MIKROTIK_PASSWORD=replace-me
|
|
65
|
+
MIKROTIK_HTTP_PORT=443
|
|
66
|
+
MIKROTIK_HTTPS=true
|
|
67
|
+
MIKROTIK_TIMEOUT_MS=10000
|
|
68
|
+
|
|
69
|
+
TELEGRAM_BOT_TOKEN=replace-me
|
|
70
|
+
TELEGRAM_ALLOWED_CHAT_IDS=123456789,987654321
|
|
71
|
+
TELEGRAM_ADMIN_CHAT_IDS=123456789
|
|
72
|
+
TELEGRAM_OPERATOR_CHAT_IDS=987654321
|
|
73
|
+
TELEGRAM_WEBHOOK_SECRET=use-at-least-16-characters
|
|
74
|
+
TELEGRAM_CONFIRMATION_TTL_SECONDS=60
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use a dedicated RouterOS user with the minimum required policy. Never commit `.env` files or expose the bot token in logs.
|
|
78
|
+
|
|
79
|
+
## Advanced RouterOS usage
|
|
80
|
+
|
|
81
|
+
The service layer is available without Telegram. This is useful for dashboards, external APIs, cron jobs, and custom workflows.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import {
|
|
85
|
+
MikroTikService,
|
|
86
|
+
RouterOsRestClient,
|
|
87
|
+
} from 'mikrotik-telegram';
|
|
88
|
+
|
|
89
|
+
const client = new RouterOsRestClient({
|
|
90
|
+
host: process.env.MIKROTIK_HOST!,
|
|
91
|
+
port: 443,
|
|
92
|
+
username: process.env.MIKROTIK_USER!,
|
|
93
|
+
password: process.env.MIKROTIK_PASSWORD!,
|
|
94
|
+
https: true,
|
|
95
|
+
timeoutMs: 10_000,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const mikrotik = new MikroTikService(client);
|
|
99
|
+
|
|
100
|
+
const preview = await mikrotik.generateVouchers({
|
|
101
|
+
count: 25,
|
|
102
|
+
prefix: 'EVENT',
|
|
103
|
+
profile: 'guest-2h',
|
|
104
|
+
limitUptime: '2h',
|
|
105
|
+
dryRun: true,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const users = await mikrotik.listHotspotUsers({ profile: 'guest-2h', limit: 100 });
|
|
109
|
+
console.log({ preview, users });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Telegram webhook
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { createConfigFromEnv, createRuntime, createWebhookHandler } from 'mikrotik-telegram';
|
|
116
|
+
|
|
117
|
+
const runtime = createRuntime(createConfigFromEnv(process.env));
|
|
118
|
+
export const POST = createWebhookHandler(runtime.handleUpdate, {
|
|
119
|
+
secret: process.env.TELEGRAM_WEBHOOK_SECRET,
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Protected operations
|
|
124
|
+
|
|
125
|
+
Destructive actions should be exposed through a protected action instead of being called directly from an untrusted command.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { createProtectedAction } from 'mikrotik-telegram';
|
|
129
|
+
|
|
130
|
+
const disconnect = createProtectedAction(
|
|
131
|
+
'disconnect-session',
|
|
132
|
+
async (input: { sessionId: string }) => ({
|
|
133
|
+
ok: true,
|
|
134
|
+
data: await mikrotik.disconnectSession(input.sessionId),
|
|
135
|
+
}),
|
|
136
|
+
{
|
|
137
|
+
requiredRole: 'operator',
|
|
138
|
+
requiresConfirmation: true,
|
|
139
|
+
maxBatchSize: 1,
|
|
140
|
+
confirm: async () => ({ ok: true }),
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Runtime support
|
|
146
|
+
|
|
147
|
+
The library owns RouterOS and Telegram behavior but does not own your web framework or process lifecycle.
|
|
148
|
+
|
|
149
|
+
| Runtime | Entry point |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| Node.js / Bun | `createNodeBot()` / `createBunBot()` |
|
|
152
|
+
| Fetch-compatible webhook | `createWebhookHandler()` |
|
|
153
|
+
| Serverless | `createServerlessHandler()` |
|
|
154
|
+
| Worker | `createWorkerHandler()` / `createWorkerRuntime()` |
|
|
155
|
+
| Cron | `createCronRunner()` |
|
|
156
|
+
| Long polling | `runPolling()` |
|
|
157
|
+
|
|
158
|
+
## Security model
|
|
159
|
+
|
|
160
|
+
- Chat allowlists are checked before command execution.
|
|
161
|
+
- Roles are ordered as `viewer < operator < admin`.
|
|
162
|
+
- Confirmation records are chat-bound and expire automatically.
|
|
163
|
+
- Destructive batches can enforce a maximum size.
|
|
164
|
+
- RouterOS and Telegram credentials stay in caller-managed configuration.
|
|
165
|
+
- Audit records can be exported as JSON or CSV.
|
|
166
|
+
|
|
167
|
+
## API surface
|
|
168
|
+
|
|
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
|
+
|
|
171
|
+
## Versioning
|
|
172
|
+
|
|
173
|
+
This package follows [Semantic Versioning](https://semver.org/):
|
|
174
|
+
|
|
175
|
+
- `MAJOR`: breaking API or behavior changes.
|
|
176
|
+
- `MINOR`: backwards-compatible features and new exports.
|
|
177
|
+
- `PATCH`: backwards-compatible fixes, security patches, and documentation corrections.
|
|
178
|
+
|
|
179
|
+
The npm package is currently `0.1.0`, so the API may still evolve before `1.0.0`. Releases should update `version` in `package.json`, add a changelog entry, run the checks below, and publish a new immutable npm version.
|
|
180
|
+
|
|
181
|
+
## Local package checks
|
|
182
|
+
|
|
183
|
+
These commands run from this directory and do not require the root application configuration:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npm install
|
|
187
|
+
npm run build
|
|
188
|
+
npm pack --dry-run
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The contract suite uses Bun: `bun install`, `bun run build`, and `bun test`.
|
|
192
|
+
|
|
193
|
+
## Publishing
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npm login
|
|
197
|
+
npm run build
|
|
198
|
+
npm pack --dry-run
|
|
199
|
+
npm publish --access public
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The published tarball contains only `dist`, `README.md`, and `LICENSE`; source tests and the parent application are excluded.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export type ActionExecutionResult<TData = unknown> = {
|
|
3
|
+
readonly ok: boolean;
|
|
4
|
+
readonly data?: TData;
|
|
5
|
+
readonly error?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ActionContext = {
|
|
8
|
+
readonly chatId: string;
|
|
9
|
+
readonly userId?: string;
|
|
10
|
+
readonly role: 'admin' | 'operator' | 'viewer';
|
|
11
|
+
};
|
|
12
|
+
export type ProtectedActionOptions<TInput, TData> = {
|
|
13
|
+
readonly requiredRole: ActionContext['role'];
|
|
14
|
+
readonly requiresConfirmation?: boolean;
|
|
15
|
+
readonly maxBatchSize?: number;
|
|
16
|
+
readonly confirm?: (input: TInput, context: ActionContext) => Promise<ActionExecutionResult<void>>;
|
|
17
|
+
};
|
|
18
|
+
export declare function createProtectedAction<TInput, TData>(name: string, action: (input: TInput, context: ActionContext) => Promise<ActionExecutionResult<TData>>, options: ProtectedActionOptions<TInput, TData>): (input: TInput, context: ActionContext) => Promise<ActionExecutionResult<TData>>;
|
|
19
|
+
export type RouterActionCatalog = {
|
|
20
|
+
readonly health: () => Promise<ActionExecutionResult<unknown>>;
|
|
21
|
+
readonly disconnectUser: (input: {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
reason?: string;
|
|
24
|
+
}) => Promise<ActionExecutionResult<unknown>>;
|
|
25
|
+
readonly banUser: (input: {
|
|
26
|
+
address?: string;
|
|
27
|
+
user?: string;
|
|
28
|
+
comment?: string;
|
|
29
|
+
timeout?: string;
|
|
30
|
+
}) => Promise<ActionExecutionResult<unknown>>;
|
|
31
|
+
};
|
|
32
|
+
export declare function createRouterActionCatalog(actions: Partial<RouterActionCatalog>): RouterActionCatalog;
|
|
33
|
+
export declare const ProtectedActionInputSchema: z.ZodObject<{
|
|
34
|
+
address: z.ZodOptional<z.ZodString>;
|
|
35
|
+
user: z.ZodOptional<z.ZodString>;
|
|
36
|
+
comment: z.ZodOptional<z.ZodString>;
|
|
37
|
+
timeout: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, z.core.$strip>;
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function createProtectedAction(name, action, options) {
|
|
3
|
+
const roleWeight = { viewer: 1, operator: 2, admin: 3 };
|
|
4
|
+
return async (input, context) => {
|
|
5
|
+
if (roleWeight[context.role] < roleWeight[options.requiredRole]) {
|
|
6
|
+
return { ok: false, error: `Role insuffisant pour ${name}.` };
|
|
7
|
+
}
|
|
8
|
+
const inputList = Array.isArray(input?.ids)
|
|
9
|
+
? input.ids.length
|
|
10
|
+
: undefined;
|
|
11
|
+
if (options.maxBatchSize !== undefined && inputList !== undefined && inputList > options.maxBatchSize) {
|
|
12
|
+
return { ok: false, error: `Lot destructif trop large pour ${name}. Maximum autorisé: ${options.maxBatchSize}.` };
|
|
13
|
+
}
|
|
14
|
+
if (options.requiresConfirmation) {
|
|
15
|
+
const confirmation = options.confirm ? await options.confirm(input, context) : { ok: true };
|
|
16
|
+
if (!confirmation.ok) {
|
|
17
|
+
return { ok: false, error: confirmation.error || `Confirmation refusée pour ${name}.` };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return action(input, context);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function createRouterActionCatalog(actions) {
|
|
24
|
+
const catalog = {
|
|
25
|
+
health: actions.health ?? (async () => ({ ok: true, data: { status: 'healthy' } })),
|
|
26
|
+
disconnectUser: actions.disconnectUser ?? (async (input) => ({ ok: true, data: { success: true, id: input.sessionId, operation: 'disconnect-session' } })),
|
|
27
|
+
banUser: actions.banUser ?? (async (input) => ({ ok: true, data: { success: true, address: input.address || input.user, ruleId: 'generated' } })),
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
health: () => catalog.health(),
|
|
31
|
+
disconnectUser: (input) => catalog.disconnectUser(input),
|
|
32
|
+
banUser: (input) => catalog.banUser(input),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export const ProtectedActionInputSchema = z.object({
|
|
36
|
+
address: z.string().trim().min(1).optional(),
|
|
37
|
+
user: z.string().trim().min(1).optional(),
|
|
38
|
+
comment: z.string().max(255).optional(),
|
|
39
|
+
timeout: z.string().trim().min(1).optional(),
|
|
40
|
+
});
|
package/dist/alerts.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { HotspotProfile, SystemHealth, TrafficMetrics } from './types';
|
|
3
|
+
export declare const AlertSeveritySchema: z.ZodEnum<{
|
|
4
|
+
info: "info";
|
|
5
|
+
warning: "warning";
|
|
6
|
+
critical: "critical";
|
|
7
|
+
}>;
|
|
8
|
+
export type AlertSeverity = z.infer<typeof AlertSeveritySchema>;
|
|
9
|
+
export interface Alert {
|
|
10
|
+
readonly key: string;
|
|
11
|
+
readonly severity: AlertSeverity;
|
|
12
|
+
readonly title: string;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
readonly createdAt: string;
|
|
15
|
+
readonly resolved?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface AlertThresholds {
|
|
18
|
+
readonly cpuWarningPercent: number;
|
|
19
|
+
readonly cpuCriticalPercent: number;
|
|
20
|
+
readonly freeMemoryWarningBytes: number;
|
|
21
|
+
readonly freeMemoryCriticalBytes: number;
|
|
22
|
+
readonly freeStorageWarningBytes: number;
|
|
23
|
+
readonly criticalStockMultiplier: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const DefaultAlertThresholds: AlertThresholds;
|
|
26
|
+
export interface RouterSnapshot {
|
|
27
|
+
readonly online: boolean;
|
|
28
|
+
readonly health?: SystemHealth;
|
|
29
|
+
readonly activeSessions?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface AlertEvaluation {
|
|
32
|
+
readonly alerts: readonly Alert[];
|
|
33
|
+
readonly resolvedKeys: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
export declare function evaluateRouterAnomalies(snapshot: RouterSnapshot, thresholds?: AlertThresholds, now?: Date): AlertEvaluation;
|
|
36
|
+
export declare function evaluateStockAnomalies(profiles: readonly HotspotProfile[], now?: Date): AlertEvaluation;
|
|
37
|
+
export declare function evaluateTrafficAnomalies(metrics: TrafficMetrics, now?: Date): AlertEvaluation;
|
|
38
|
+
export interface AlertState {
|
|
39
|
+
readonly lastEmittedAt: Readonly<Record<string, number>>;
|
|
40
|
+
}
|
|
41
|
+
export declare class AlertEngine {
|
|
42
|
+
private readonly cooldownMs;
|
|
43
|
+
private readonly now;
|
|
44
|
+
private readonly lastEmittedAt;
|
|
45
|
+
private readonly repeatCount;
|
|
46
|
+
constructor(cooldownMs?: number, now?: () => number);
|
|
47
|
+
filter(alerts: readonly Alert[]): AlertEvaluation;
|
|
48
|
+
reset(key?: string): void;
|
|
49
|
+
snapshot(): AlertState;
|
|
50
|
+
}
|
package/dist/alerts.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const AlertSeveritySchema = z.enum(['info', 'warning', 'critical']);
|
|
3
|
+
export const DefaultAlertThresholds = {
|
|
4
|
+
cpuWarningPercent: 70,
|
|
5
|
+
cpuCriticalPercent: 90,
|
|
6
|
+
freeMemoryWarningBytes: 32 * 1024 * 1024,
|
|
7
|
+
freeMemoryCriticalBytes: 16 * 1024 * 1024,
|
|
8
|
+
freeStorageWarningBytes: 64 * 1024 * 1024,
|
|
9
|
+
criticalStockMultiplier: 1,
|
|
10
|
+
};
|
|
11
|
+
export function evaluateRouterAnomalies(snapshot, thresholds = DefaultAlertThresholds, now = new Date()) {
|
|
12
|
+
const alerts = [];
|
|
13
|
+
const add = (key, severity, title, message) => alerts.push({ key, severity, title, message, createdAt: now.toISOString() });
|
|
14
|
+
if (!snapshot.online)
|
|
15
|
+
add('router.offline', 'critical', 'Routeur hors ligne', 'Le routeur ne répond plus aux contrôles de disponibilité REST.');
|
|
16
|
+
if (snapshot.health) {
|
|
17
|
+
const health = snapshot.health;
|
|
18
|
+
if (health.cpuLoadPercent >= thresholds.cpuCriticalPercent)
|
|
19
|
+
add('router.cpu.critical', 'critical', 'CPU critique', `Charge CPU à ${health.cpuLoadPercent}%.`);
|
|
20
|
+
else if (health.cpuLoadPercent >= thresholds.cpuWarningPercent)
|
|
21
|
+
add('router.cpu.warning', 'warning', 'CPU élevé', `Charge CPU à ${health.cpuLoadPercent}%.`);
|
|
22
|
+
if (health.freeMemoryBytes <= thresholds.freeMemoryCriticalBytes)
|
|
23
|
+
add('router.memory.critical', 'critical', 'Mémoire critique', `Mémoire libre: ${health.freeMemoryBytes} octets.`);
|
|
24
|
+
else if (health.freeMemoryBytes <= thresholds.freeMemoryWarningBytes)
|
|
25
|
+
add('router.memory.warning', 'warning', 'Mémoire faible', `Mémoire libre: ${health.freeMemoryBytes} octets.`);
|
|
26
|
+
if (health.freeStorageBytes <= thresholds.freeStorageWarningBytes)
|
|
27
|
+
add('router.storage.warning', 'warning', 'Stockage faible', `Stockage libre: ${health.freeStorageBytes} octets.`);
|
|
28
|
+
}
|
|
29
|
+
return { alerts, resolvedKeys: [] };
|
|
30
|
+
}
|
|
31
|
+
export function evaluateStockAnomalies(profiles, now = new Date()) {
|
|
32
|
+
const alerts = profiles.filter((profile) => profile.sharedUsers !== undefined && profile.sharedUsers <= 0).map((profile) => ({
|
|
33
|
+
key: `profile.stock.${profile.id}`, severity: 'warning', title: 'Profil sans capacité configurée',
|
|
34
|
+
message: `Le profil ${profile.name} ne possède pas de capacité partagée positive.`, createdAt: now.toISOString(),
|
|
35
|
+
}));
|
|
36
|
+
return { alerts, resolvedKeys: [] };
|
|
37
|
+
}
|
|
38
|
+
export function evaluateTrafficAnomalies(metrics, now = new Date()) {
|
|
39
|
+
const alerts = [];
|
|
40
|
+
const add = (key, severity, title, message) => alerts.push({
|
|
41
|
+
key, severity, title, message, createdAt: now.toISOString(),
|
|
42
|
+
});
|
|
43
|
+
if (!metrics.online) {
|
|
44
|
+
add('traffic.offline', 'critical', 'Flux hors ligne', 'Le routeur n’est plus en mesure de signaler les usages réseau.');
|
|
45
|
+
}
|
|
46
|
+
if (metrics.rxBytesPerSecond !== undefined && metrics.rxBytesPerSecond > 8_000_000) {
|
|
47
|
+
add('traffic.download.warning', 'warning', 'Téléchargement élevé', `Le trafic entrant est de ${metrics.rxBytesPerSecond} octets/s.`);
|
|
48
|
+
}
|
|
49
|
+
if (metrics.txBytesPerSecond !== undefined && metrics.txBytesPerSecond > 8_000_000) {
|
|
50
|
+
add('traffic.upload.warning', 'warning', 'Téléversement élevé', `Le trafic sortant est de ${metrics.txBytesPerSecond} octets/s.`);
|
|
51
|
+
}
|
|
52
|
+
if (metrics.totalSessions !== undefined && metrics.peakSessions !== undefined && metrics.totalSessions >= Math.max(1, metrics.peakSessions * 0.8)) {
|
|
53
|
+
add('traffic.sessions.warning', 'warning', 'Sessions proches du seuil', `Sessions actives: ${metrics.totalSessions} / pic attendu: ${metrics.peakSessions}.`);
|
|
54
|
+
}
|
|
55
|
+
return { alerts, resolvedKeys: [] };
|
|
56
|
+
}
|
|
57
|
+
export class AlertEngine {
|
|
58
|
+
cooldownMs;
|
|
59
|
+
now;
|
|
60
|
+
lastEmittedAt = new Map();
|
|
61
|
+
repeatCount = new Map();
|
|
62
|
+
constructor(cooldownMs = 15 * 60_000, now = Date.now) {
|
|
63
|
+
this.cooldownMs = cooldownMs;
|
|
64
|
+
this.now = now;
|
|
65
|
+
}
|
|
66
|
+
filter(alerts) {
|
|
67
|
+
const emitted = [];
|
|
68
|
+
for (const alert of alerts) {
|
|
69
|
+
const previous = this.lastEmittedAt.get(alert.key);
|
|
70
|
+
const repeats = this.repeatCount.get(alert.key) ?? 0;
|
|
71
|
+
const nextRepeat = previous !== undefined && this.now() - previous < this.cooldownMs ? repeats + 1 : 0;
|
|
72
|
+
if (previous !== undefined && this.now() - previous < this.cooldownMs) {
|
|
73
|
+
this.repeatCount.set(alert.key, nextRepeat);
|
|
74
|
+
if (nextRepeat < 2)
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
this.repeatCount.set(alert.key, 0);
|
|
79
|
+
}
|
|
80
|
+
const escalated = {
|
|
81
|
+
...alert,
|
|
82
|
+
severity: alert.severity === 'warning' && nextRepeat >= 2 ? 'critical' : alert.severity,
|
|
83
|
+
title: alert.severity === 'warning' && nextRepeat >= 2 ? `${alert.title} (escalade)` : alert.title,
|
|
84
|
+
message: alert.severity === 'warning' && nextRepeat >= 2 ? `Répétition détectée: ${alert.message}` : alert.message,
|
|
85
|
+
};
|
|
86
|
+
this.lastEmittedAt.set(alert.key, this.now());
|
|
87
|
+
emitted.push(escalated);
|
|
88
|
+
}
|
|
89
|
+
return { alerts: emitted, resolvedKeys: [] };
|
|
90
|
+
}
|
|
91
|
+
reset(key) {
|
|
92
|
+
if (key) {
|
|
93
|
+
this.lastEmittedAt.delete(key);
|
|
94
|
+
this.repeatCount.delete(key);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.lastEmittedAt.clear();
|
|
98
|
+
this.repeatCount.clear();
|
|
99
|
+
}
|
|
100
|
+
snapshot() {
|
|
101
|
+
return { lastEmittedAt: Object.fromEntries(this.lastEmittedAt) };
|
|
102
|
+
}
|
|
103
|
+
}
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type AuditStatus = 'success' | 'failure' | 'pending';
|
|
2
|
+
export interface AuditEntry {
|
|
3
|
+
readonly timestamp: string;
|
|
4
|
+
readonly action: string;
|
|
5
|
+
readonly actor?: string;
|
|
6
|
+
readonly resource?: string;
|
|
7
|
+
readonly status: AuditStatus;
|
|
8
|
+
readonly metadata?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export declare class AuditLogger {
|
|
11
|
+
private readonly namespace;
|
|
12
|
+
private readonly entries;
|
|
13
|
+
constructor(namespace: string);
|
|
14
|
+
log(input: Omit<AuditEntry, 'timestamp'>): AuditEntry;
|
|
15
|
+
snapshot(): readonly AuditEntry[];
|
|
16
|
+
reset(): void;
|
|
17
|
+
get scope(): string;
|
|
18
|
+
}
|
|
19
|
+
export declare function createAuditLogger(namespace?: string): AuditLogger;
|
|
20
|
+
export declare function exportAuditLog(entries: readonly AuditEntry[], format?: 'json' | 'csv'): string;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class AuditLogger {
|
|
2
|
+
namespace;
|
|
3
|
+
entries = [];
|
|
4
|
+
constructor(namespace) {
|
|
5
|
+
this.namespace = namespace;
|
|
6
|
+
}
|
|
7
|
+
log(input) {
|
|
8
|
+
const entry = {
|
|
9
|
+
...input,
|
|
10
|
+
timestamp: new Date().toISOString(),
|
|
11
|
+
};
|
|
12
|
+
this.entries.push(entry);
|
|
13
|
+
return entry;
|
|
14
|
+
}
|
|
15
|
+
snapshot() {
|
|
16
|
+
return [...this.entries];
|
|
17
|
+
}
|
|
18
|
+
reset() {
|
|
19
|
+
this.entries.length = 0;
|
|
20
|
+
}
|
|
21
|
+
get scope() {
|
|
22
|
+
return this.namespace;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function createAuditLogger(namespace = 'default') {
|
|
26
|
+
return new AuditLogger(namespace);
|
|
27
|
+
}
|
|
28
|
+
export function exportAuditLog(entries, format = 'json') {
|
|
29
|
+
if (format === 'csv') {
|
|
30
|
+
const rows = [
|
|
31
|
+
['timestamp', 'action', 'actor', 'resource', 'status', 'metadata'],
|
|
32
|
+
...entries.map((entry) => [
|
|
33
|
+
entry.timestamp,
|
|
34
|
+
entry.action,
|
|
35
|
+
entry.actor ?? '',
|
|
36
|
+
entry.resource ?? '',
|
|
37
|
+
entry.status,
|
|
38
|
+
JSON.stringify(entry.metadata ?? {}),
|
|
39
|
+
]),
|
|
40
|
+
];
|
|
41
|
+
return rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')).join('\n');
|
|
42
|
+
}
|
|
43
|
+
return JSON.stringify(entries, null, 2);
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Alert } from './alerts';
|
|
3
|
+
export declare const AutomationActionSchema: z.ZodEnum<{
|
|
4
|
+
"health-check": "health-check";
|
|
5
|
+
"stock-check": "stock-check";
|
|
6
|
+
cleanup: "cleanup";
|
|
7
|
+
"cleanup-sessions": "cleanup-sessions";
|
|
8
|
+
backup: "backup";
|
|
9
|
+
report: "report";
|
|
10
|
+
"daily-report": "daily-report";
|
|
11
|
+
"weekly-report": "weekly-report";
|
|
12
|
+
"router-sync": "router-sync";
|
|
13
|
+
}>;
|
|
14
|
+
export type AutomationAction = z.infer<typeof AutomationActionSchema>;
|
|
15
|
+
export declare const AutomationRuleSchema: z.ZodObject<{
|
|
16
|
+
id: z.ZodString;
|
|
17
|
+
name: z.ZodString;
|
|
18
|
+
action: z.ZodEnum<{
|
|
19
|
+
"health-check": "health-check";
|
|
20
|
+
"stock-check": "stock-check";
|
|
21
|
+
cleanup: "cleanup";
|
|
22
|
+
"cleanup-sessions": "cleanup-sessions";
|
|
23
|
+
backup: "backup";
|
|
24
|
+
report: "report";
|
|
25
|
+
"daily-report": "daily-report";
|
|
26
|
+
"weekly-report": "weekly-report";
|
|
27
|
+
"router-sync": "router-sync";
|
|
28
|
+
}>;
|
|
29
|
+
intervalMs: z.ZodNumber;
|
|
30
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
31
|
+
runImmediately: z.ZodDefault<z.ZodBoolean>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export type AutomationRule = z.infer<typeof AutomationRuleSchema>;
|
|
34
|
+
export interface AutomationContext {
|
|
35
|
+
readonly now: Date;
|
|
36
|
+
readonly execute: (action: AutomationAction, rule: AutomationRule) => Promise<AutomationResult>;
|
|
37
|
+
}
|
|
38
|
+
export interface AutomationResult {
|
|
39
|
+
readonly ruleId: string;
|
|
40
|
+
readonly action: AutomationAction;
|
|
41
|
+
readonly success: boolean;
|
|
42
|
+
readonly startedAt: string;
|
|
43
|
+
readonly finishedAt: string;
|
|
44
|
+
readonly error?: string;
|
|
45
|
+
readonly alerts?: readonly Alert[];
|
|
46
|
+
}
|
|
47
|
+
export declare class AutomationEngine {
|
|
48
|
+
private readonly rules;
|
|
49
|
+
register(input: AutomationRule): AutomationRule;
|
|
50
|
+
unregister(id: string): boolean;
|
|
51
|
+
pause(id: string): AutomationRule;
|
|
52
|
+
resume(id: string): AutomationRule;
|
|
53
|
+
list(): readonly AutomationRule[];
|
|
54
|
+
run(id: string, context: AutomationContext): Promise<AutomationResult>;
|
|
55
|
+
runDue(context: AutomationContext): Promise<readonly AutomationResult[]>;
|
|
56
|
+
private getEntry;
|
|
57
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const AutomationActionSchema = z.enum([
|
|
3
|
+
'health-check',
|
|
4
|
+
'stock-check',
|
|
5
|
+
'cleanup',
|
|
6
|
+
'cleanup-sessions',
|
|
7
|
+
'backup',
|
|
8
|
+
'report',
|
|
9
|
+
'daily-report',
|
|
10
|
+
'weekly-report',
|
|
11
|
+
'router-sync',
|
|
12
|
+
]);
|
|
13
|
+
export const AutomationRuleSchema = z.object({
|
|
14
|
+
id: z.string().min(1),
|
|
15
|
+
name: z.string().min(1).max(100),
|
|
16
|
+
action: AutomationActionSchema,
|
|
17
|
+
intervalMs: z.number().int().positive(),
|
|
18
|
+
enabled: z.boolean().default(true),
|
|
19
|
+
runImmediately: z.boolean().default(false),
|
|
20
|
+
});
|
|
21
|
+
export class AutomationEngine {
|
|
22
|
+
rules = new Map();
|
|
23
|
+
register(input) {
|
|
24
|
+
const rule = AutomationRuleSchema.parse(input);
|
|
25
|
+
this.rules.set(rule.id, { rule });
|
|
26
|
+
return rule;
|
|
27
|
+
}
|
|
28
|
+
unregister(id) {
|
|
29
|
+
return this.rules.delete(z.string().min(1).parse(id));
|
|
30
|
+
}
|
|
31
|
+
pause(id) {
|
|
32
|
+
const entry = this.getEntry(id);
|
|
33
|
+
entry.rule = { ...entry.rule, enabled: false };
|
|
34
|
+
return entry.rule;
|
|
35
|
+
}
|
|
36
|
+
resume(id) {
|
|
37
|
+
const entry = this.getEntry(id);
|
|
38
|
+
entry.rule = { ...entry.rule, enabled: true };
|
|
39
|
+
return entry.rule;
|
|
40
|
+
}
|
|
41
|
+
list() {
|
|
42
|
+
return [...this.rules.values()].map((entry) => entry.rule);
|
|
43
|
+
}
|
|
44
|
+
async run(id, context) {
|
|
45
|
+
const entry = this.getEntry(id);
|
|
46
|
+
const startedAt = context.now.toISOString();
|
|
47
|
+
if (!entry.rule.enabled)
|
|
48
|
+
return { ruleId: id, action: entry.rule.action, success: false, startedAt, finishedAt: context.now.toISOString(), error: 'Automation désactivée.' };
|
|
49
|
+
try {
|
|
50
|
+
const result = await context.execute(entry.rule.action, entry.rule);
|
|
51
|
+
entry.lastRunAt = context.now.getTime();
|
|
52
|
+
return { ...result, ruleId: id, action: entry.rule.action, startedAt, finishedAt: new Date().toISOString() };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
entry.lastRunAt = context.now.getTime();
|
|
56
|
+
return { ruleId: id, action: entry.rule.action, success: false, startedAt, finishedAt: new Date().toISOString(), error: error instanceof Error ? error.message : String(error) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async runDue(context) {
|
|
60
|
+
const now = context.now.getTime();
|
|
61
|
+
const due = [...this.rules.values()].filter((entry) => entry.rule.enabled && (entry.rule.runImmediately || entry.lastRunAt === undefined || now - entry.lastRunAt >= entry.rule.intervalMs));
|
|
62
|
+
return Promise.all(due.map((entry) => this.run(entry.rule.id, context)));
|
|
63
|
+
}
|
|
64
|
+
getEntry(id) {
|
|
65
|
+
const entry = this.rules.get(z.string().min(1).parse(id));
|
|
66
|
+
if (!entry)
|
|
67
|
+
throw new Error(`Automation inconnue: ${id}`);
|
|
68
|
+
return entry;
|
|
69
|
+
}
|
|
70
|
+
}
|