@anomira/node-sdk 0.1.9 → 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/README.md +273 -130
- package/dist/index.cjs +32 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,184 +2,296 @@
|
|
|
2
2
|
|
|
3
3
|
Drop-in API security monitoring for Node.js. Detect brute force, credential stuffing, account takeover, data scraping, path traversal, XSS, geo-velocity attacks, and more — in real time.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/@anomira/node-sdk)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
5
8
|
## Install
|
|
6
9
|
|
|
7
10
|
```bash
|
|
8
11
|
npm install @anomira/node-sdk
|
|
12
|
+
# or
|
|
13
|
+
yarn add @anomira/node-sdk
|
|
14
|
+
# or
|
|
15
|
+
pnpm add @anomira/node-sdk
|
|
9
16
|
```
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
**Requirements:** Node.js 18+, ESM or CommonJS.
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
import { Anomira } from "@anomira/node-sdk";
|
|
20
|
+
---
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
## Quick Start (5 minutes)
|
|
23
|
+
|
|
24
|
+
### 1. Set your environment variables
|
|
25
|
+
|
|
26
|
+
Copy these into your `.env` file. Get the values from your [Anomira dashboard](https://app.anomira.io) under **Apps → Setup**.
|
|
27
|
+
|
|
28
|
+
```env
|
|
29
|
+
ANOMIRA_API_KEY=ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
30
|
+
ANOMIRA_APP_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
22
31
|
```
|
|
23
32
|
|
|
33
|
+
> **Never hardcode these values.** Use environment variables or a secrets manager. Run `npx @anomira/node-sdk scan .` to catch any leaks before you commit.
|
|
34
|
+
|
|
35
|
+
### 2. Add the middleware — pick your framework
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
24
39
|
## Express
|
|
25
40
|
|
|
26
|
-
|
|
41
|
+
The middleware goes **after** `express.json()` and **before** your routes so it can read request bodies.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// app.ts (TypeScript)
|
|
27
45
|
import express from "express";
|
|
28
46
|
import { Anomira } from "@anomira/node-sdk";
|
|
29
47
|
|
|
30
48
|
const app = express();
|
|
31
49
|
|
|
32
|
-
const
|
|
33
|
-
apiKey:
|
|
34
|
-
appId:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
captureConsole: true, // forwards console.log/warn/error to Logs dashboard
|
|
38
|
-
service: "my-api",
|
|
39
|
-
detect: {
|
|
40
|
-
bruteForce: true,
|
|
41
|
-
rateAbuse: true,
|
|
42
|
-
pathTraversal: true,
|
|
43
|
-
xss: true,
|
|
44
|
-
scanDetection: true,
|
|
45
|
-
geoVelocity: true,
|
|
46
|
-
},
|
|
50
|
+
const anomira = new Anomira({
|
|
51
|
+
apiKey: process.env.ANOMIRA_API_KEY!,
|
|
52
|
+
appId: process.env.ANOMIRA_APP_ID!,
|
|
53
|
+
service: "my-api", // appears in the Logs dashboard
|
|
54
|
+
captureConsole: true, // forwards console.log/warn/error to Logs
|
|
47
55
|
});
|
|
48
56
|
|
|
49
57
|
app.use(express.json());
|
|
50
|
-
app.use(
|
|
58
|
+
app.use(anomira.express()); // ← single line — instruments all routes
|
|
59
|
+
|
|
60
|
+
// Your routes
|
|
61
|
+
app.post("/api/auth/login", (req, res) => { /* ... */ });
|
|
62
|
+
app.get("/api/users/:id", (req, res) => { /* ... */ });
|
|
63
|
+
|
|
64
|
+
// Always flush before shutdown
|
|
65
|
+
process.on("SIGTERM", async () => {
|
|
66
|
+
await anomira.flush();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
app.listen(3000, () => console.log("API running on :3000"));
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**CommonJS:**
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
// app.js
|
|
77
|
+
const express = require("express");
|
|
78
|
+
const { Anomira } = require("@anomira/node-sdk");
|
|
79
|
+
|
|
80
|
+
const app = express();
|
|
81
|
+
const anomira = new Anomira({
|
|
82
|
+
apiKey: process.env.ANOMIRA_API_KEY,
|
|
83
|
+
appId: process.env.ANOMIRA_APP_ID,
|
|
84
|
+
});
|
|
51
85
|
|
|
86
|
+
app.use(express.json());
|
|
87
|
+
app.use(anomira.express());
|
|
52
88
|
app.listen(3000);
|
|
53
89
|
```
|
|
54
90
|
|
|
91
|
+
---
|
|
92
|
+
|
|
55
93
|
## Fastify
|
|
56
94
|
|
|
57
|
-
```
|
|
95
|
+
```ts
|
|
96
|
+
// server.ts
|
|
58
97
|
import Fastify from "fastify";
|
|
59
98
|
import { Anomira } from "@anomira/node-sdk";
|
|
60
99
|
|
|
61
|
-
const app = Fastify();
|
|
100
|
+
const app = Fastify({ logger: true });
|
|
62
101
|
|
|
63
|
-
const
|
|
64
|
-
apiKey:
|
|
65
|
-
appId:
|
|
66
|
-
|
|
102
|
+
const anomira = new Anomira({
|
|
103
|
+
apiKey: process.env.ANOMIRA_API_KEY!,
|
|
104
|
+
appId: process.env.ANOMIRA_APP_ID!,
|
|
105
|
+
service: "my-api",
|
|
67
106
|
});
|
|
68
107
|
|
|
69
|
-
|
|
108
|
+
// Register BEFORE your route plugins
|
|
109
|
+
await app.register(anomira.fastify());
|
|
110
|
+
|
|
111
|
+
// Your routes
|
|
112
|
+
app.post("/api/auth/login", async (req, reply) => { /* ... */ });
|
|
113
|
+
app.get("/api/users/:id", async (req, reply) => { /* ... */ });
|
|
70
114
|
|
|
71
|
-
|
|
115
|
+
process.on("SIGTERM", async () => {
|
|
116
|
+
await anomira.flush();
|
|
117
|
+
await app.close();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await app.listen({ port: 3000, host: "0.0.0.0" });
|
|
72
121
|
```
|
|
73
122
|
|
|
74
|
-
|
|
123
|
+
---
|
|
75
124
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
125
|
+
## What the middleware captures automatically
|
|
126
|
+
|
|
127
|
+
Once registered, the middleware records **every request** with zero additional code:
|
|
128
|
+
|
|
129
|
+
| Signal | Description |
|
|
130
|
+
|---|---|
|
|
131
|
+
| HTTP method, path, status | Full request log in **API Events** |
|
|
132
|
+
| Source IP + geolocation | Country, city, lat/lng |
|
|
133
|
+
| Latency | `latencyMs` per request |
|
|
134
|
+
| Brute force | Repeated failures on the same endpoint |
|
|
135
|
+
| Rate abuse | Unusually high request rate from one IP |
|
|
136
|
+
| Path traversal | `../`, `%2e%2e/`, null bytes in paths |
|
|
137
|
+
| XSS | Script tags and injection payloads in bodies |
|
|
138
|
+
| Scanner/bot probing | Systematic enumeration of endpoints |
|
|
139
|
+
| Geo-velocity | Impossible logins from two countries within minutes |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Manual event tracking
|
|
144
|
+
|
|
145
|
+
Use these when the middleware can't infer the event on its own — e.g., application-level failures.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// Credential stuffing / failed login attempt
|
|
149
|
+
await anomira.trackLogin({
|
|
150
|
+
ip: req.ip,
|
|
151
|
+
userId: req.body.email, // email or user ID
|
|
152
|
+
success: false, // false = failed attempt
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Successful login (enables geo-velocity tracking)
|
|
156
|
+
await anomira.trackLogin({
|
|
157
|
+
ip: req.ip,
|
|
158
|
+
userId: user.id,
|
|
159
|
+
success: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Failed OTP — otp_flood detection
|
|
163
|
+
anomira.track("auth.otp.failed", {
|
|
79
164
|
ip: req.ip,
|
|
80
165
|
userId: req.body.phone,
|
|
81
166
|
meta: { endpoint: "/api/verify-otp" },
|
|
82
167
|
});
|
|
83
168
|
|
|
84
|
-
//
|
|
85
|
-
|
|
169
|
+
// SIM swap detection — call after any phone-based auth
|
|
170
|
+
anomira.trackPhoneAuth({ ip: req.ip, userId: user.id, phone: user.phone });
|
|
86
171
|
|
|
87
|
-
//
|
|
88
|
-
|
|
172
|
+
// Account takeover signal — e.g., password changed from new IP
|
|
173
|
+
anomira.track("auth.account.takeover", {
|
|
174
|
+
ip: req.ip,
|
|
175
|
+
userId: user.id,
|
|
176
|
+
meta: { reason: "password_changed_new_ip" },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Webhook replay — call when you detect a replayed webhook signature
|
|
180
|
+
anomira.track("webhook.replay.detected", {
|
|
181
|
+
ip: req.ip,
|
|
182
|
+
meta: { webhookId: req.headers["x-webhook-id"] },
|
|
183
|
+
});
|
|
89
184
|
```
|
|
90
185
|
|
|
91
|
-
|
|
186
|
+
---
|
|
92
187
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
188
|
+
## Structured logging
|
|
189
|
+
|
|
190
|
+
Replace `console.log` calls with `anomira.log` to push structured logs to the **Logs dashboard** with level, service name, and metadata.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
anomira.log("info", "User registered", { userId: user.id, plan: "starter" });
|
|
194
|
+
anomira.log("warn", "Slow DB query", { queryMs: 1240, table: "transactions" });
|
|
195
|
+
anomira.log("error", "Payment failed", { reason: err.message, amount: 500_00 });
|
|
196
|
+
anomira.log("debug", "Cache miss", { key: cacheKey });
|
|
97
197
|
```
|
|
98
198
|
|
|
99
|
-
|
|
199
|
+
Or set `captureConsole: true` in the constructor to automatically forward all `console.*` calls — no code changes needed.
|
|
100
200
|
|
|
101
|
-
|
|
201
|
+
---
|
|
102
202
|
|
|
103
|
-
|
|
104
|
-
if (sentinel.isBlocked(req.ip)) {
|
|
105
|
-
return res.status(403).json({ error: "Forbidden" });
|
|
106
|
-
}
|
|
203
|
+
## Blocklist & firewall check
|
|
107
204
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
205
|
+
The SDK syncs your dashboard's blocked IPs and firewall rules every 60 seconds. Use these checks in a gateway middleware before your routes:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
app.use((req, res, next) => {
|
|
209
|
+
// Check if IP is manually blocked in the dashboard
|
|
210
|
+
if (anomira.isBlocked(req.ip)) {
|
|
211
|
+
return res.status(403).json({ error: "Forbidden" });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check firewall rules (pattern-based: path, method, header, body)
|
|
215
|
+
const match = anomira.matchFirewallRule({
|
|
216
|
+
url: req.originalUrl,
|
|
217
|
+
method: req.method,
|
|
218
|
+
body: req.body,
|
|
219
|
+
headers: req.headers,
|
|
220
|
+
ip: req.ip,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (match) {
|
|
224
|
+
if (match.rule.action === "block") {
|
|
225
|
+
return res.status(403).json({ error: "Request blocked", rule: match.rule.name });
|
|
226
|
+
}
|
|
227
|
+
// action === "flag" — let it through but the dashboard will flag it
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
next();
|
|
113
231
|
});
|
|
114
|
-
if (match?.rule.action === "block") {
|
|
115
|
-
return res.status(403).json({ error: "Blocked by firewall rule" });
|
|
116
|
-
}
|
|
117
232
|
```
|
|
118
233
|
|
|
119
|
-
|
|
234
|
+
---
|
|
120
235
|
|
|
121
|
-
|
|
236
|
+
## Shadow endpoint detection
|
|
122
237
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{ method: "
|
|
129
|
-
{ method: "
|
|
130
|
-
{ method: "POST", path: "/api/
|
|
131
|
-
{ method: "GET", path: "/api/
|
|
238
|
+
Register your known API routes once on startup. Anomira will flag any traffic to undeclared endpoints in the **API Surface** view — a leading indicator of probing and abuse.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
// Call this once, after all your routes are defined
|
|
242
|
+
await anomira.declareEndpoints([
|
|
243
|
+
{ method: "POST", path: "/api/auth/login", auth: false },
|
|
244
|
+
{ method: "POST", path: "/api/auth/register", auth: false },
|
|
245
|
+
{ method: "POST", path: "/api/auth/forgot", auth: false },
|
|
246
|
+
{ method: "GET", path: "/api/users/:id", auth: true },
|
|
247
|
+
{ method: "PUT", path: "/api/users/:id", auth: true },
|
|
248
|
+
{ method: "GET", path: "/api/orders", auth: true },
|
|
249
|
+
{ method: "POST", path: "/api/orders", auth: true },
|
|
250
|
+
{ method: "GET", path: "/api/health", auth: false },
|
|
132
251
|
]);
|
|
133
252
|
```
|
|
134
253
|
|
|
135
|
-
Express-style `:param` segments are normalized automatically
|
|
254
|
+
Express-style `:param` segments (`/users/:id`) are normalized automatically to match real traffic. Call this after your routes are registered so all paths are included.
|
|
136
255
|
|
|
137
|
-
|
|
256
|
+
---
|
|
138
257
|
|
|
139
|
-
|
|
258
|
+
## Secret scanner CLI
|
|
140
259
|
|
|
141
|
-
|
|
260
|
+
Scan your codebase for leaked secrets, API keys, and PII before they reach production.
|
|
142
261
|
|
|
143
|
-
Scan your codebase for hardcoded secrets, API keys, BVN/NIN numbers, and PII before they reach production.
|
|
144
|
-
|
|
145
|
-
Uses three detection layers:
|
|
146
|
-
- **secretlint** — 50+ service-specific rules (AWS, GCP, GitHub, Stripe, Slack, Twilio, SendGrid, PostgreSQL connection strings, and more)
|
|
147
|
-
- **Custom patterns** — Nigerian PII (BVN/NIN), card PANs, phone numbers
|
|
148
|
-
- **Entropy analysis** (`--strict`) — catches unknown high-entropy secrets with no known prefix, using Shannon entropy scoring
|
|
149
|
-
|
|
150
|
-
**Run without installing:**
|
|
151
262
|
```bash
|
|
263
|
+
# One-off scan (no install needed)
|
|
152
264
|
npx @anomira/node-sdk scan ./src
|
|
153
|
-
```
|
|
154
265
|
|
|
155
|
-
|
|
156
|
-
```bash
|
|
266
|
+
# If the package is already installed
|
|
157
267
|
npx anomira scan ./src
|
|
158
268
|
```
|
|
159
269
|
|
|
160
270
|
**Options:**
|
|
271
|
+
|
|
161
272
|
```bash
|
|
162
273
|
npx @anomira/node-sdk scan ./src # scan a directory
|
|
163
274
|
npx @anomira/node-sdk scan . # scan entire project
|
|
164
|
-
npx @anomira/node-sdk scan ./src --strict #
|
|
165
|
-
npx @anomira/node-sdk scan ./src --json # machine-readable JSON
|
|
166
|
-
npx @anomira/node-sdk scan ./src --quiet # only
|
|
275
|
+
npx @anomira/node-sdk scan ./src --strict # entropy analysis (catches unknown secrets)
|
|
276
|
+
npx @anomira/node-sdk scan ./src --json # machine-readable JSON for CI
|
|
277
|
+
npx @anomira/node-sdk scan ./src --quiet # violations only, no header
|
|
167
278
|
```
|
|
168
279
|
|
|
169
|
-
**
|
|
280
|
+
**Detects:**
|
|
170
281
|
|
|
171
282
|
| Category | Examples |
|
|
172
283
|
|---|---|
|
|
173
|
-
| Cloud
|
|
174
|
-
| Source control | GitHub
|
|
175
|
-
| Payment | Stripe
|
|
176
|
-
| Communication | Slack
|
|
177
|
-
| Database | Connection strings with embedded passwords
|
|
178
|
-
| Auth | JWT tokens, generic API keys
|
|
179
|
-
|
|
|
180
|
-
| Unknown
|
|
181
|
-
|
|
182
|
-
|
|
284
|
+
| Cloud | AWS access keys, GCP service accounts, Azure connection strings |
|
|
285
|
+
| Source control | GitHub (`ghp_`), GitLab (`glpat-`), NPM (`npm_`) tokens |
|
|
286
|
+
| Payment | Stripe (`sk_live_`), Paystack secret keys |
|
|
287
|
+
| Communication | Slack (`xoxb-`), Twilio, SendGrid |
|
|
288
|
+
| Database | Connection strings with embedded passwords |
|
|
289
|
+
| Auth | JWT tokens, bearer tokens, generic API keys |
|
|
290
|
+
| PII | BVN/NIN (11-digit), card PANs, Nigerian phone numbers |
|
|
291
|
+
| Unknown | High-entropy strings on secret-like variable names (`--strict`) |
|
|
292
|
+
|
|
293
|
+
Add to CI/CD — exits `1` if violations are found:
|
|
294
|
+
|
|
183
295
|
```json
|
|
184
296
|
{
|
|
185
297
|
"scripts": {
|
|
@@ -188,44 +300,75 @@ npx @anomira/node-sdk scan ./src --quiet # only print violations, no header
|
|
|
188
300
|
}
|
|
189
301
|
```
|
|
190
302
|
|
|
191
|
-
|
|
303
|
+
---
|
|
192
304
|
|
|
193
|
-
##
|
|
305
|
+
## Graceful shutdown
|
|
194
306
|
|
|
195
|
-
|
|
196
|
-
|---|---|
|
|
197
|
-
| `SENTINEL_API_KEY` | Your Anomira API key |
|
|
198
|
-
| `SENTINEL_APP_ID` | Your Anomira app ID |
|
|
199
|
-
| `SENTINEL_INGEST_URL` | Ingest endpoint (from your dashboard) |
|
|
307
|
+
Always flush the event buffer before your process exits. Unflushed events may be lost on a hard kill.
|
|
200
308
|
|
|
201
|
-
|
|
309
|
+
```ts
|
|
310
|
+
process.on("SIGTERM", async () => {
|
|
311
|
+
await anomira.flush();
|
|
312
|
+
process.exit(0);
|
|
313
|
+
});
|
|
202
314
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
| `debug` | `boolean` | `false` | Log SDK activity to console |
|
|
209
|
-
| `captureConsole` | `boolean` | `false` | Forward `console.*` calls to the Logs dashboard |
|
|
210
|
-
| `service` | `string` | `"app"` | Service name tag for logs |
|
|
211
|
-
| `detect.bruteForce` | `boolean` | `true` | Detect brute force login attempts |
|
|
212
|
-
| `detect.rateAbuse` | `boolean` | `true` | Detect rate limit abuse |
|
|
213
|
-
| `detect.pathTraversal` | `boolean` | `true` | Detect path traversal attempts |
|
|
214
|
-
| `detect.xss` | `boolean` | `true` | Detect XSS in request bodies |
|
|
215
|
-
| `detect.scanDetection` | `boolean` | `true` | Detect scanner/bot probing |
|
|
216
|
-
| `detect.geoVelocity` | `boolean` | `true` | Detect impossible travel between logins |
|
|
315
|
+
// For Fastify with lifecycle hooks:
|
|
316
|
+
app.addHook("onClose", async () => {
|
|
317
|
+
await anomira.flush();
|
|
318
|
+
});
|
|
319
|
+
```
|
|
217
320
|
|
|
218
|
-
|
|
321
|
+
---
|
|
219
322
|
|
|
220
|
-
|
|
323
|
+
## Environment variables reference
|
|
221
324
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
325
|
+
| Variable | Required | Description |
|
|
326
|
+
|---|---|---|
|
|
327
|
+
| `ANOMIRA_API_KEY` | ✅ | Your API key — from dashboard under **Apps → Setup** |
|
|
328
|
+
| `ANOMIRA_APP_ID` | ✅ | Your app ID — from dashboard under **Apps → Setup** |
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Configuration reference
|
|
333
|
+
|
|
334
|
+
| Option | Type | Default | Description |
|
|
335
|
+
|---|---|---|---|
|
|
336
|
+
| `apiKey` | `string` | — | API key (required) |
|
|
337
|
+
| `appId` | `string` | — | App ID (required) |
|
|
338
|
+
| `debug` | `boolean` | `false` | Log SDK activity to console |
|
|
339
|
+
| `service` | `string` | `"app"` | Service name tag on all log entries |
|
|
340
|
+
| `captureConsole` | `boolean` | `false` | Forward `console.*` to Logs dashboard |
|
|
341
|
+
| `detect.bruteForce` | `boolean` | `true` | Brute force detection on auth endpoints |
|
|
342
|
+
| `detect.rateAbuse` | `boolean` | `true` | High-rate abuse from single IP |
|
|
343
|
+
| `detect.pathTraversal` | `boolean` | `true` | Path traversal payloads in URLs |
|
|
344
|
+
| `detect.xss` | `boolean` | `true` | XSS payloads in request bodies |
|
|
345
|
+
| `detect.scanDetection` | `boolean` | `true` | Endpoint scanner / bot probing |
|
|
346
|
+
| `detect.geoVelocity` | `boolean` | `true` | Impossible travel between logins |
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Troubleshooting
|
|
351
|
+
|
|
352
|
+
**No events showing in the dashboard?**
|
|
353
|
+
1. Set `debug: true` in the constructor — the SDK will log every event it sends to the console.
|
|
354
|
+
2. Check that `ANOMIRA_API_KEY` and `ANOMIRA_APP_ID` are set in your process environment (`console.log(process.env.ANOMIRA_API_KEY)`).
|
|
355
|
+
3. Confirm the middleware is registered **before** your routes and **after** body parsers.
|
|
356
|
+
4. Make sure you're not blocking outbound HTTPS traffic to `api.anomira.io`.
|
|
357
|
+
|
|
358
|
+
**TypeScript errors on `req.ip`?**
|
|
359
|
+
Express types sometimes don't include `ip` directly. Use `(req as express.Request).ip ?? req.socket.remoteAddress ?? "0.0.0.0"`.
|
|
360
|
+
|
|
361
|
+
**`flush()` taking too long on shutdown?**
|
|
362
|
+
The flush waits for in-flight HTTP requests to complete. If your process needs to exit fast, you can add a timeout:
|
|
363
|
+
```ts
|
|
364
|
+
await Promise.race([
|
|
365
|
+
anomira.flush(),
|
|
366
|
+
new Promise((resolve) => setTimeout(resolve, 3000)),
|
|
367
|
+
]);
|
|
227
368
|
```
|
|
228
369
|
|
|
370
|
+
---
|
|
371
|
+
|
|
229
372
|
## License
|
|
230
373
|
|
|
231
374
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -671,6 +671,30 @@ var AnomiraClient = class {
|
|
|
671
671
|
if (this.disabled) return false;
|
|
672
672
|
return this.blockedIpCache.has(ip);
|
|
673
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Fire-and-forget: report a blocked-IP attempt to the ingest server so the
|
|
676
|
+
* dashboard can show that the block is actively working.
|
|
677
|
+
* Called automatically by the Express/Fastify middleware — no manual call needed.
|
|
678
|
+
*/
|
|
679
|
+
reportBlockedHit(ip, meta) {
|
|
680
|
+
if (this.disabled) return;
|
|
681
|
+
const blockedHitUrl = this.config.ingestUrl.replace(/\/v1\/events$/, "/v1/blocked-hit");
|
|
682
|
+
fetch(blockedHitUrl, {
|
|
683
|
+
method: "POST",
|
|
684
|
+
headers: {
|
|
685
|
+
"Content-Type": "application/json",
|
|
686
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
687
|
+
},
|
|
688
|
+
body: JSON.stringify({
|
|
689
|
+
appId: this.config.appId,
|
|
690
|
+
ip,
|
|
691
|
+
method: meta.method,
|
|
692
|
+
endpoint: meta.url,
|
|
693
|
+
userAgent: meta.userAgent,
|
|
694
|
+
ts: Date.now()
|
|
695
|
+
})
|
|
696
|
+
}).catch(() => null);
|
|
697
|
+
}
|
|
674
698
|
/** Evaluate firewall rules against a request. Returns the matched rule or null. Synchronous. */
|
|
675
699
|
matchFirewallRule(req) {
|
|
676
700
|
return this.#matchFirewallRule(req);
|
|
@@ -863,6 +887,10 @@ function createExpressMiddleware(client) {
|
|
|
863
887
|
const startMs = Date.now();
|
|
864
888
|
const ip = client.config.getIp(req);
|
|
865
889
|
if (client.isBlocked(ip)) {
|
|
890
|
+
const method_ = req["method"]?.toUpperCase() ?? "GET";
|
|
891
|
+
const url_ = req["originalUrl"] ?? req["url"] ?? "/";
|
|
892
|
+
const ua_ = req["headers"]?.["user-agent"] ?? "";
|
|
893
|
+
client.reportBlockedHit(ip, { method: method_, url: url_, userAgent: ua_ });
|
|
866
894
|
const res_ = res;
|
|
867
895
|
if (typeof res_["status"] === "function") {
|
|
868
896
|
res_["status"](403);
|
|
@@ -938,7 +966,11 @@ function createFastifyPlugin(client) {
|
|
|
938
966
|
return async function sentinelFastifyPlugin(fastify) {
|
|
939
967
|
fastify.addHook("onRequest", (req, reply) => {
|
|
940
968
|
const ip = client.config.getIp(req);
|
|
969
|
+
const url_ = req["url"] ?? "/";
|
|
970
|
+
const method_ = req["method"]?.toUpperCase() ?? "GET";
|
|
971
|
+
const hdrs_ = req["headers"];
|
|
941
972
|
if (client.isBlocked(ip)) {
|
|
973
|
+
client.reportBlockedHit(ip, { method: method_, url: url_, userAgent: hdrs_?.["user-agent"] ?? "" });
|
|
942
974
|
const rep = reply;
|
|
943
975
|
if (typeof rep["code"] === "function") {
|
|
944
976
|
const chained = rep["code"](403);
|