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 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
+ [![npm version](https://img.shields.io/npm/v/mikrotik-telegram?logo=npm)](https://www.npmjs.com/package/mikrotik-telegram)
4
+ [![npm downloads](https://img.shields.io/npm/dm/mikrotik-telegram?logo=npm)](https://www.npmjs.com/package/mikrotik-telegram)
5
+ [![Node.js](https://img.shields.io/node/v/mikrotik-telegram?logo=node.js)](https://nodejs.org/)
6
+ [![License](https://img.shields.io/npm/l/mikrotik-telegram)](./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>;
@@ -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
+ });
@@ -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
+ }
@@ -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
+ }