@liberstudio/cloudflare-list 2.4.7 → 3.0.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/README.md CHANGED
@@ -47,6 +47,8 @@ Il modulo ignora automaticamente:
47
47
  - Action: `Block`
48
48
  6. Salvare la regola
49
49
 
50
+ Con più account Cloudflare ripetere i passi per ogni account: ognuno ha la sua lista, il suo token e la sua regola WAF.
51
+
50
52
  ---
51
53
 
52
54
  ## Installazione
@@ -67,9 +69,10 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
67
69
  @Module({
68
70
  imports: [
69
71
  CloudflareAttacksModule.forRoot({
70
- accountId: "abc123",
71
- listId: "def456",
72
- apiToken: "your-cloudflare-api-token",
72
+ accounts: [
73
+ { accountId: "abc123", listId: "def456", apiToken: "token-account-1" },
74
+ { accountId: "ghi789", listId: "jkl012", apiToken: "token-account-2" },
75
+ ],
73
76
  comment: "Blocked by liberstudio/cloudflare-list",
74
77
  logPath: "/var/log/nestjs-attacks.log",
75
78
  allowedIps: ["1.2.3.4", "10.0.0.0/8"],
@@ -94,9 +97,18 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
94
97
  imports: [ConfigModule],
95
98
  inject: [ConfigService],
96
99
  useFactory: (config: ConfigService) => ({
97
- apiToken: config.getOrThrow<string>("CLOUDFLARE_API_TOKEN"),
98
- accountId: config.getOrThrow<string>("CLOUDFLARE_ACCOUNT_ID"),
99
- listId: config.getOrThrow<string>("CLOUDFLARE_LIST_ID"),
100
+ accounts: [
101
+ {
102
+ apiToken: config.getOrThrow<string>("CLOUDFLARE_API_TOKEN"),
103
+ accountId: config.getOrThrow<string>("CLOUDFLARE_ACCOUNT_ID"),
104
+ listId: config.getOrThrow<string>("CLOUDFLARE_LIST_ID"),
105
+ },
106
+ {
107
+ apiToken: config.getOrThrow<string>("CLOUDFLARE_2_API_TOKEN"),
108
+ accountId: config.getOrThrow<string>("CLOUDFLARE_2_ACCOUNT_ID"),
109
+ listId: config.getOrThrow<string>("CLOUDFLARE_2_LIST_ID"),
110
+ },
111
+ ],
100
112
  comment: config.get<string>("CLOUDFLARE_LIST_COMMENT") || "Blocked",
101
113
  logPath: config.get<string>("CLOUDFLARE_LIST_LOG_PATH") || "/var/log/nestjs-attacks.log",
102
114
  allowedIps: ["1.2.3.4", "10.0.0.0/8"],
@@ -106,6 +118,7 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
106
118
  telegramNotify: {
107
119
  chatId: config.getOrThrow<string>("TELEGRAM_NOTIFY_CHAT_ID"),
108
120
  },
121
+ notifyProvider: config.get<"whatsapp" | "telegram">("NOTIFY_PROVIDER"), // assente → scelta automatica
109
122
  }),
110
123
  }),
111
124
  ],
@@ -119,9 +132,7 @@ export class AppModule {}
119
132
 
120
133
  | Opzione | Tipo | Obbligatorio | Descrizione |
121
134
  |---|---|---|---|
122
- | `apiToken` | `string` | si | Token API Cloudflare |
123
- | `accountId` | `string` | si | ID account Cloudflare |
124
- | `listId` | `string` | si | ID della IP List Cloudflare |
135
+ | `accounts` | `CloudflareAccount[]` | si | Account Cloudflare su cui bannare (almeno uno), vedi sotto |
125
136
  | `comment` | `string` | si | Commento aggiunto ad ogni IP bloccato |
126
137
  | `logPath` | `string` | si | Percorso del file di log degli attacchi |
127
138
  | `allowedIps` | `string[]` | no | IP o CIDR mai bloccati (whitelist, supporta IPv4 e IPv6) |
@@ -132,6 +143,38 @@ export class AppModule {}
132
143
  | `telegramNotify` | `{ chatId: string }` | no | Invia una notifica Telegram quando un IP viene bannato |
133
144
  | `notifyProvider` | `"whatsapp" \| "telegram"` | no | Canale di notifica da usare (default: vedi sotto) |
134
145
 
146
+ ### `accounts` — ban su più account Cloudflare
147
+
148
+ Ogni elemento di `accounts` è un `CloudflareAccount`:
149
+
150
+ | Campo | Tipo | Obbligatorio | Descrizione |
151
+ |---|---|---|---|
152
+ | `apiToken` | `string` | si | Token API Cloudflare con accesso all'account |
153
+ | `accountId` | `string` | si | ID account Cloudflare |
154
+ | `listId` | `string` | si | ID della IP List dell'account |
155
+
156
+ Ad ogni ban l'IP (o la subnet) viene aggiunto **in parallelo** alle liste di tutti gli account:
157
+
158
+ - se un account fallisce, l'errore viene loggato con il suo `accountId` e gli altri proseguono;
159
+ - se falliscono **tutti**, `updateIpList()` lancia `HttpException` con il primo errore;
160
+ - `updateIpList()` e `blockIp()` ritornano un `CloudflareResponse[]` con le risposte degli account riusciti.
161
+
162
+ Se lo stesso token ha accesso a più account, basta ripeterlo in ogni elemento.
163
+
164
+ ### Migrazione dalla 2.x
165
+
166
+ La 3.0.0 è un breaking change: `apiToken`, `accountId` e `listId` non stanno più alla radice delle opzioni, ma dentro `accounts`.
167
+
168
+ ```typescript
169
+ // 2.x
170
+ { apiToken: "...", accountId: "...", listId: "...", comment: "...", logPath: "..." }
171
+
172
+ // 3.x
173
+ { accounts: [{ apiToken: "...", accountId: "...", listId: "..." }], comment: "...", logPath: "..." }
174
+ ```
175
+
176
+ Inoltre `updateIpList()` ritorna `CloudflareResponse[]` invece di `CloudflareResponse`, e `blockIp()` ritorna `CloudflareResponse[] | null`.
177
+
135
178
  ### `notifyProvider` — scelta del canale di notifica
136
179
 
137
180
  Ad ogni ban viene inviata **una sola** notifica, sul canale scelto:
@@ -156,6 +199,7 @@ Per ricavare il `chatId`: scrivi un messaggio al bot (o aggiungilo al gruppo), p
156
199
  ```env
157
200
  TELEGRAM_BOT_TOKEN=123456:ABC-your-bot-token
158
201
  TELEGRAM_NOTIFY_CHAT_ID=123456789
202
+ NOTIFY_PROVIDER=telegram # opzionale: whatsapp | telegram
159
203
  ```
160
204
 
161
205
  ### `whatsappNotify` — notifiche ban su WhatsApp
@@ -174,8 +218,11 @@ Il messaggio inviato sarà:
174
218
  Provenienza: Blocked by liberstudio/cloudflare-list
175
219
  IP: 1.2.3.4
176
220
  Subnet bloccata: 1.2.0.0/16
221
+ Account: 2/2
177
222
  ```
178
223
 
224
+ `Account` indica su quanti account configurati il ban è andato a buon fine. La notifica è sempre **una** per ban.
225
+
179
226
  Variabili d'ambiente richieste (lette automaticamente da `process.env`):
180
227
  ```env
181
228
  WA_URL=https://messaggistica.liberdesign.eu
@@ -7,11 +7,13 @@ export declare class AttacksService {
7
7
  private readonly API_URL;
8
8
  private readonly ASN_LOOKUP_URL;
9
9
  constructor(httpService: HttpService, options: CloudflareAttacksOptions);
10
- blockIp(rawIp: string): Promise<CloudflareResponse | null>;
10
+ blockIp(rawIp: string): Promise<CloudflareResponse[] | null>;
11
11
  private sendNotification;
12
12
  private isWhatsappConfigured;
13
13
  private sendTelegramNotification;
14
14
  private sendWhatsappNotification;
15
15
  private lookupAsnRange;
16
- updateIpList(cidr: string): Promise<CloudflareResponse>;
16
+ /** Aggiunge il CIDR alle liste di tutti gli account; ritorna le risposte degli account riusciti. */
17
+ updateIpList(cidr: string): Promise<CloudflareResponse[]>;
18
+ private addToAccountList;
17
19
  }
@@ -47,7 +47,7 @@ let AttacksService = class AttacksService {
47
47
  if (asnRange && !(await (0, utils_1.googleBotOverlapsCidr)(asnRange))) {
48
48
  this.logger.error(`ASN range trovato per ${rawIp}: ${asnRange} — blocco intera subnet`);
49
49
  const result = await this.updateIpList(asnRange);
50
- this.sendNotification(rawIp, asnRange);
50
+ this.sendNotification(rawIp, asnRange, result.length);
51
51
  return result;
52
52
  }
53
53
  if (asnRange) {
@@ -57,11 +57,11 @@ let AttacksService = class AttacksService {
57
57
  if (!cidr)
58
58
  throw new Error(`IP non valido: ${rawIp}`);
59
59
  const result = await this.updateIpList(cidr);
60
- this.sendNotification(rawIp, cidr);
60
+ this.sendNotification(rawIp, cidr, result.length);
61
61
  return result;
62
62
  }
63
- sendNotification(rawIp, cidr) {
64
- const message = `🚨 IP bannato su Cloudflare\nProvenienza: ${this.options.comment}\nIP: ${rawIp}\nSubnet bloccata: ${cidr}`;
63
+ sendNotification(rawIp, cidr, okAccounts) {
64
+ const message = `🚨 IP bannato su Cloudflare\nProvenienza: ${this.options.comment}\nIP: ${rawIp}\nSubnet bloccata: ${cidr}\nAccount: ${okAccounts}/${this.options.accounts.length}`;
65
65
  const provider = this.options.notifyProvider ?? (this.isWhatsappConfigured() ? "whatsapp" : "telegram");
66
66
  if (provider === "whatsapp")
67
67
  this.sendWhatsappNotification(message);
@@ -83,7 +83,8 @@ let AttacksService = class AttacksService {
83
83
  }
84
84
  (0, rxjs_1.firstValueFrom)(this.httpService.post(`https://api.telegram.org/bot${token}/sendMessage`, { chat_id: tg.chatId, text: message })).catch((err) => {
85
85
  const msg = err instanceof Error ? err.message : "Errore sconosciuto";
86
- this.logger.warn(`Notifica Telegram fallita: ${msg}`);
86
+ const description = axios_2.default.isAxiosError(err) ? err.response?.data?.description : undefined;
87
+ this.logger.warn(`Notifica Telegram fallita: ${msg}${description ? ` (${description})` : ""}`);
87
88
  });
88
89
  }
89
90
  sendWhatsappNotification(message) {
@@ -113,11 +114,30 @@ let AttacksService = class AttacksService {
113
114
  return null;
114
115
  }
115
116
  }
117
+ /** Aggiunge il CIDR alle liste di tutti gli account; ritorna le risposte degli account riusciti. */
116
118
  async updateIpList(cidr) {
117
- const { accountId, listId, apiToken, comment } = this.options;
119
+ const { accounts } = this.options;
120
+ this.logger.error(`Aggiungo ${cidr} alle liste Cloudflare (${accounts.length} account)`);
121
+ const results = await Promise.allSettled(accounts.map((account) => this.addToAccountList(account, cidr)));
122
+ const ok = [];
123
+ let firstError;
124
+ results.forEach((r, i) => {
125
+ if (r.status === "fulfilled") {
126
+ ok.push(r.value);
127
+ return;
128
+ }
129
+ firstError ??= r.reason;
130
+ const msg = r.reason instanceof Error ? r.reason.message : "Errore sconosciuto";
131
+ this.logger.error(`Aggiunta di ${cidr} fallita sull'account ${accounts[i].accountId}: ${msg}`);
132
+ });
133
+ if (ok.length === 0)
134
+ throw firstError;
135
+ return ok;
136
+ }
137
+ async addToAccountList(account, cidr) {
138
+ const { accountId, listId, apiToken } = account;
118
139
  const url = `${this.API_URL}/${accountId}/rules/lists/${listId}/items`;
119
- this.logger.error(`Aggiungo ${cidr} alla lista Cloudflare`);
120
- const body = [{ ip: cidr, comment }];
140
+ const body = [{ ip: cidr, comment: this.options.comment }];
121
141
  try {
122
142
  const response = await (0, rxjs_1.firstValueFrom)(this.httpService.post(url, body, {
123
143
  headers: {
@@ -5,10 +5,13 @@ export interface TelegramNotifyOptions {
5
5
  chatId: string;
6
6
  }
7
7
  export type NotifyProvider = "whatsapp" | "telegram";
8
- export interface CloudflareAttacksOptions {
8
+ export interface CloudflareAccount {
9
9
  apiToken: string;
10
10
  accountId: string;
11
11
  listId: string;
12
+ }
13
+ export interface CloudflareAttacksOptions {
14
+ accounts: CloudflareAccount[];
12
15
  comment: string;
13
16
  logPath: string;
14
17
  allowedIps?: string[];
@@ -20,4 +20,10 @@ if (minor >= 10) {
20
20
  pkg.version = `${major}.${minor}.${patch}`;
21
21
  // Scrittura package.json
22
22
  (0, node_fs_1.writeFileSync)(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
23
+ // Allineamento package-lock.json (versione radice e del pacchetto "")
24
+ const lockPath = (0, node_path_1.resolve)(__dirname, '../../package-lock.json');
25
+ const lock = JSON.parse((0, node_fs_1.readFileSync)(lockPath, 'utf8'));
26
+ lock.version = pkg.version;
27
+ lock.packages[''].version = pkg.version;
28
+ (0, node_fs_1.writeFileSync)(lockPath, JSON.stringify(lock, null, 2) + '\n');
23
29
  console.log(`Version updated to ${pkg.version}`);
@@ -1,16 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateOptions = validateOptions;
4
+ function isNonEmptyString(value) {
5
+ return typeof value === "string" && value.trim().length > 0;
6
+ }
4
7
  function validateOptions(opzioni) {
5
8
  if (!opzioni)
6
9
  throw new Error("CloudflareAttacksOptions è obbligatorio");
7
- const campiObbligatori = ["apiToken", "accountId", "listId", "comment", "logPath"];
10
+ const campiObbligatori = ["comment", "logPath"];
8
11
  for (const campo of campiObbligatori) {
9
- const valore = opzioni[campo];
10
- if (typeof valore !== "string" || valore.trim().length === 0) {
12
+ if (!isNonEmptyString(opzioni[campo])) {
11
13
  throw new Error(`CloudflareAttacksOptions.${campo} deve essere una stringa non vuota`);
12
14
  }
13
15
  }
16
+ if (!Array.isArray(opzioni.accounts) || opzioni.accounts.length === 0) {
17
+ throw new Error("CloudflareAttacksOptions.accounts deve essere un array non vuoto");
18
+ }
19
+ const campiAccount = ["apiToken", "accountId", "listId"];
20
+ opzioni.accounts.forEach((account, i) => {
21
+ for (const campo of campiAccount) {
22
+ if (!isNonEmptyString(account?.[campo])) {
23
+ throw new Error(`CloudflareAttacksOptions.accounts[${i}].${campo} deve essere una stringa non vuota`);
24
+ }
25
+ }
26
+ });
14
27
  const provider = opzioni.notifyProvider;
15
28
  if (provider !== undefined && provider !== "whatsapp" && provider !== "telegram") {
16
29
  throw new Error(`CloudflareAttacksOptions.notifyProvider deve essere "whatsapp" o "telegram"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liberstudio/cloudflare-list",
3
- "version": "2.4.7",
3
+ "version": "3.0.0",
4
4
  "description": "Modulo NestJS per gestione IP List Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",