@anomira/node-sdk 0.1.9 → 0.2.2
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 +297 -130
- package/dist/index.cjs +1204 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -6
- package/dist/index.d.ts +102 -6
- package/dist/index.js +1204 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,184 +2,320 @@
|
|
|
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 **after your auth middleware** so it can read the authenticated user, and **before** your routes.
|
|
42
|
+
|
|
43
|
+
### How user identity is captured automatically
|
|
44
|
+
|
|
45
|
+
The SDK automatically extracts the authenticated user ID from the request — no configuration required. It tries the following sources in order:
|
|
46
|
+
|
|
47
|
+
| Priority | Source | Set by |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| 1 | `req.user.id` / `.sub` / `.userId` / `._id` / `.uid` | Passport.js, express-jwt v6, Firebase Admin, @fastify/jwt |
|
|
50
|
+
| 2 | `req.auth.sub` / `.id` / `.userId` | express-jwt v7+ |
|
|
51
|
+
| 3 | `req.userId` / `req.accountId` / `req.customerId` | Custom middleware |
|
|
52
|
+
| 4 | `req.session.userId` / `req.session.user.id` | express-session |
|
|
53
|
+
| 5 | JWT decode from `Authorization: Bearer ...` | **Automatic fallback — works even without explicit auth middleware** |
|
|
54
|
+
|
|
55
|
+
Tier 5 is the safety net: if your auth middleware hasn't set `req.user` yet, the SDK decodes the JWT token in the `Authorization` header itself (without verifying the signature — it only reads the `sub` / `id` claim). This means user tracking works even if middleware registration order is incorrect.
|
|
56
|
+
|
|
57
|
+
**The only requirement:** if you use a custom auth pattern not listed above, pass `getUserId`:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const anomira = new Anomira({
|
|
61
|
+
apiKey: process.env.ANOMIRA_API_KEY!,
|
|
62
|
+
appId: process.env.ANOMIRA_APP_ID!,
|
|
63
|
+
getUserId: (req) => (req as any).myCustomField?.userId,
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// app.ts (TypeScript)
|
|
27
69
|
import express from "express";
|
|
28
70
|
import { Anomira } from "@anomira/node-sdk";
|
|
29
71
|
|
|
30
72
|
const app = express();
|
|
31
73
|
|
|
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
|
-
},
|
|
74
|
+
const anomira = new Anomira({
|
|
75
|
+
apiKey: process.env.ANOMIRA_API_KEY!,
|
|
76
|
+
appId: process.env.ANOMIRA_APP_ID!,
|
|
77
|
+
service: "my-api", // appears in the Logs dashboard
|
|
78
|
+
captureConsole: true, // forwards console.log/warn/error to Logs
|
|
47
79
|
});
|
|
48
80
|
|
|
49
81
|
app.use(express.json());
|
|
50
|
-
app.use(
|
|
82
|
+
app.use(anomira.express()); // ← single line — instruments all routes
|
|
83
|
+
|
|
84
|
+
// Your routes
|
|
85
|
+
app.post("/api/auth/login", (req, res) => { /* ... */ });
|
|
86
|
+
app.get("/api/users/:id", (req, res) => { /* ... */ });
|
|
87
|
+
|
|
88
|
+
// Always flush before shutdown
|
|
89
|
+
process.on("SIGTERM", async () => {
|
|
90
|
+
await anomira.flush();
|
|
91
|
+
process.exit(0);
|
|
92
|
+
});
|
|
51
93
|
|
|
94
|
+
app.listen(3000, () => console.log("API running on :3000"));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**CommonJS:**
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// app.js
|
|
101
|
+
const express = require("express");
|
|
102
|
+
const { Anomira } = require("@anomira/node-sdk");
|
|
103
|
+
|
|
104
|
+
const app = express();
|
|
105
|
+
const anomira = new Anomira({
|
|
106
|
+
apiKey: process.env.ANOMIRA_API_KEY,
|
|
107
|
+
appId: process.env.ANOMIRA_APP_ID,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.use(express.json());
|
|
111
|
+
app.use(anomira.express());
|
|
52
112
|
app.listen(3000);
|
|
53
113
|
```
|
|
54
114
|
|
|
115
|
+
---
|
|
116
|
+
|
|
55
117
|
## Fastify
|
|
56
118
|
|
|
57
|
-
```
|
|
119
|
+
```ts
|
|
120
|
+
// server.ts
|
|
58
121
|
import Fastify from "fastify";
|
|
59
122
|
import { Anomira } from "@anomira/node-sdk";
|
|
60
123
|
|
|
61
|
-
const app = Fastify();
|
|
124
|
+
const app = Fastify({ logger: true });
|
|
62
125
|
|
|
63
|
-
const
|
|
64
|
-
apiKey:
|
|
65
|
-
appId:
|
|
66
|
-
|
|
126
|
+
const anomira = new Anomira({
|
|
127
|
+
apiKey: process.env.ANOMIRA_API_KEY!,
|
|
128
|
+
appId: process.env.ANOMIRA_APP_ID!,
|
|
129
|
+
service: "my-api",
|
|
67
130
|
});
|
|
68
131
|
|
|
69
|
-
|
|
132
|
+
// Register BEFORE your route plugins
|
|
133
|
+
await app.register(anomira.fastify());
|
|
134
|
+
|
|
135
|
+
// Your routes
|
|
136
|
+
app.post("/api/auth/login", async (req, reply) => { /* ... */ });
|
|
137
|
+
app.get("/api/users/:id", async (req, reply) => { /* ... */ });
|
|
70
138
|
|
|
71
|
-
|
|
139
|
+
process.on("SIGTERM", async () => {
|
|
140
|
+
await anomira.flush();
|
|
141
|
+
await app.close();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await app.listen({ port: 3000, host: "0.0.0.0" });
|
|
72
145
|
```
|
|
73
146
|
|
|
74
|
-
|
|
147
|
+
---
|
|
75
148
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
149
|
+
## What the middleware captures automatically
|
|
150
|
+
|
|
151
|
+
Once registered, the middleware records **every request** with zero additional code:
|
|
152
|
+
|
|
153
|
+
| Signal | Description |
|
|
154
|
+
|---|---|
|
|
155
|
+
| HTTP method, path, status | Full request log in **API Events** |
|
|
156
|
+
| Source IP + geolocation | Country, city, lat/lng |
|
|
157
|
+
| Latency | `latencyMs` per request |
|
|
158
|
+
| Brute force | Repeated failures on the same endpoint |
|
|
159
|
+
| Rate abuse | Unusually high request rate from one IP |
|
|
160
|
+
| Path traversal | `../`, `%2e%2e/`, null bytes in paths |
|
|
161
|
+
| XSS | Script tags and injection payloads in bodies |
|
|
162
|
+
| Scanner/bot probing | Systematic enumeration of endpoints |
|
|
163
|
+
| Geo-velocity | Impossible logins from two countries within minutes |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Manual event tracking
|
|
168
|
+
|
|
169
|
+
Use these when the middleware can't infer the event on its own — e.g., application-level failures.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// Credential stuffing / failed login attempt
|
|
173
|
+
await anomira.trackLogin({
|
|
174
|
+
ip: req.ip,
|
|
175
|
+
userId: req.body.email, // email or user ID
|
|
176
|
+
success: false, // false = failed attempt
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Successful login (enables geo-velocity tracking)
|
|
180
|
+
await anomira.trackLogin({
|
|
181
|
+
ip: req.ip,
|
|
182
|
+
userId: user.id,
|
|
183
|
+
success: true,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Failed OTP — otp_flood detection
|
|
187
|
+
anomira.track("auth.otp.failed", {
|
|
79
188
|
ip: req.ip,
|
|
80
189
|
userId: req.body.phone,
|
|
81
190
|
meta: { endpoint: "/api/verify-otp" },
|
|
82
191
|
});
|
|
83
192
|
|
|
84
|
-
//
|
|
85
|
-
|
|
193
|
+
// SIM swap detection — call after any phone-based auth
|
|
194
|
+
anomira.trackPhoneAuth({ ip: req.ip, userId: user.id, phone: user.phone });
|
|
195
|
+
|
|
196
|
+
// Account takeover signal — e.g., password changed from new IP
|
|
197
|
+
anomira.track("auth.account.takeover", {
|
|
198
|
+
ip: req.ip,
|
|
199
|
+
userId: user.id,
|
|
200
|
+
meta: { reason: "password_changed_new_ip" },
|
|
201
|
+
});
|
|
86
202
|
|
|
87
|
-
//
|
|
88
|
-
|
|
203
|
+
// Webhook replay — call when you detect a replayed webhook signature
|
|
204
|
+
anomira.track("webhook.replay.detected", {
|
|
205
|
+
ip: req.ip,
|
|
206
|
+
meta: { webhookId: req.headers["x-webhook-id"] },
|
|
207
|
+
});
|
|
89
208
|
```
|
|
90
209
|
|
|
91
|
-
|
|
210
|
+
---
|
|
92
211
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
212
|
+
## Structured logging
|
|
213
|
+
|
|
214
|
+
Replace `console.log` calls with `anomira.log` to push structured logs to the **Logs dashboard** with level, service name, and metadata.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
anomira.log("info", "User registered", { userId: user.id, plan: "starter" });
|
|
218
|
+
anomira.log("warn", "Slow DB query", { queryMs: 1240, table: "transactions" });
|
|
219
|
+
anomira.log("error", "Payment failed", { reason: err.message, amount: 500_00 });
|
|
220
|
+
anomira.log("debug", "Cache miss", { key: cacheKey });
|
|
97
221
|
```
|
|
98
222
|
|
|
99
|
-
|
|
223
|
+
Or set `captureConsole: true` in the constructor to automatically forward all `console.*` calls — no code changes needed.
|
|
100
224
|
|
|
101
|
-
|
|
225
|
+
---
|
|
102
226
|
|
|
103
|
-
|
|
104
|
-
if (sentinel.isBlocked(req.ip)) {
|
|
105
|
-
return res.status(403).json({ error: "Forbidden" });
|
|
106
|
-
}
|
|
227
|
+
## Blocklist & firewall check
|
|
107
228
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
229
|
+
The SDK syncs your dashboard's blocked IPs and firewall rules every 60 seconds. Use these checks in a gateway middleware before your routes:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
app.use((req, res, next) => {
|
|
233
|
+
// Check if IP is manually blocked in the dashboard
|
|
234
|
+
if (anomira.isBlocked(req.ip)) {
|
|
235
|
+
return res.status(403).json({ error: "Forbidden" });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check firewall rules (pattern-based: path, method, header, body)
|
|
239
|
+
const match = anomira.matchFirewallRule({
|
|
240
|
+
url: req.originalUrl,
|
|
241
|
+
method: req.method,
|
|
242
|
+
body: req.body,
|
|
243
|
+
headers: req.headers,
|
|
244
|
+
ip: req.ip,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (match) {
|
|
248
|
+
if (match.rule.action === "block") {
|
|
249
|
+
return res.status(403).json({ error: "Request blocked", rule: match.rule.name });
|
|
250
|
+
}
|
|
251
|
+
// action === "flag" — let it through but the dashboard will flag it
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
next();
|
|
113
255
|
});
|
|
114
|
-
if (match?.rule.action === "block") {
|
|
115
|
-
return res.status(403).json({ error: "Blocked by firewall rule" });
|
|
116
|
-
}
|
|
117
256
|
```
|
|
118
257
|
|
|
119
|
-
|
|
258
|
+
---
|
|
120
259
|
|
|
121
|
-
|
|
260
|
+
## Shadow endpoint detection
|
|
122
261
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{ method: "
|
|
129
|
-
{ method: "
|
|
130
|
-
{ method: "POST", path: "/api/
|
|
131
|
-
{ method: "GET", path: "/api/
|
|
262
|
+
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.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
// Call this once, after all your routes are defined
|
|
266
|
+
await anomira.declareEndpoints([
|
|
267
|
+
{ method: "POST", path: "/api/auth/login", auth: false },
|
|
268
|
+
{ method: "POST", path: "/api/auth/register", auth: false },
|
|
269
|
+
{ method: "POST", path: "/api/auth/forgot", auth: false },
|
|
270
|
+
{ method: "GET", path: "/api/users/:id", auth: true },
|
|
271
|
+
{ method: "PUT", path: "/api/users/:id", auth: true },
|
|
272
|
+
{ method: "GET", path: "/api/orders", auth: true },
|
|
273
|
+
{ method: "POST", path: "/api/orders", auth: true },
|
|
274
|
+
{ method: "GET", path: "/api/health", auth: false },
|
|
132
275
|
]);
|
|
133
276
|
```
|
|
134
277
|
|
|
135
|
-
Express-style `:param` segments are normalized automatically
|
|
278
|
+
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
279
|
|
|
137
|
-
|
|
280
|
+
---
|
|
138
281
|
|
|
139
|
-
|
|
282
|
+
## Secret scanner CLI
|
|
140
283
|
|
|
141
|
-
|
|
284
|
+
Scan your codebase for leaked secrets, API keys, and PII before they reach production.
|
|
142
285
|
|
|
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
286
|
```bash
|
|
287
|
+
# One-off scan (no install needed)
|
|
152
288
|
npx @anomira/node-sdk scan ./src
|
|
153
|
-
```
|
|
154
289
|
|
|
155
|
-
|
|
156
|
-
```bash
|
|
290
|
+
# If the package is already installed
|
|
157
291
|
npx anomira scan ./src
|
|
158
292
|
```
|
|
159
293
|
|
|
160
294
|
**Options:**
|
|
295
|
+
|
|
161
296
|
```bash
|
|
162
297
|
npx @anomira/node-sdk scan ./src # scan a directory
|
|
163
298
|
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
|
|
299
|
+
npx @anomira/node-sdk scan ./src --strict # entropy analysis (catches unknown secrets)
|
|
300
|
+
npx @anomira/node-sdk scan ./src --json # machine-readable JSON for CI
|
|
301
|
+
npx @anomira/node-sdk scan ./src --quiet # violations only, no header
|
|
167
302
|
```
|
|
168
303
|
|
|
169
|
-
**
|
|
304
|
+
**Detects:**
|
|
170
305
|
|
|
171
306
|
| Category | Examples |
|
|
172
307
|
|---|---|
|
|
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
|
-
|
|
308
|
+
| Cloud | AWS access keys, GCP service accounts, Azure connection strings |
|
|
309
|
+
| Source control | GitHub (`ghp_`), GitLab (`glpat-`), NPM (`npm_`) tokens |
|
|
310
|
+
| Payment | Stripe (`sk_live_`), Paystack secret keys |
|
|
311
|
+
| Communication | Slack (`xoxb-`), Twilio, SendGrid |
|
|
312
|
+
| Database | Connection strings with embedded passwords |
|
|
313
|
+
| Auth | JWT tokens, bearer tokens, generic API keys |
|
|
314
|
+
| PII | BVN/NIN (11-digit), card PANs, Nigerian phone numbers |
|
|
315
|
+
| Unknown | High-entropy strings on secret-like variable names (`--strict`) |
|
|
316
|
+
|
|
317
|
+
Add to CI/CD — exits `1` if violations are found:
|
|
318
|
+
|
|
183
319
|
```json
|
|
184
320
|
{
|
|
185
321
|
"scripts": {
|
|
@@ -188,44 +324,75 @@ npx @anomira/node-sdk scan ./src --quiet # only print violations, no header
|
|
|
188
324
|
}
|
|
189
325
|
```
|
|
190
326
|
|
|
191
|
-
|
|
327
|
+
---
|
|
192
328
|
|
|
193
|
-
##
|
|
329
|
+
## Graceful shutdown
|
|
194
330
|
|
|
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) |
|
|
331
|
+
Always flush the event buffer before your process exits. Unflushed events may be lost on a hard kill.
|
|
200
332
|
|
|
201
|
-
|
|
333
|
+
```ts
|
|
334
|
+
process.on("SIGTERM", async () => {
|
|
335
|
+
await anomira.flush();
|
|
336
|
+
process.exit(0);
|
|
337
|
+
});
|
|
202
338
|
|
|
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 |
|
|
339
|
+
// For Fastify with lifecycle hooks:
|
|
340
|
+
app.addHook("onClose", async () => {
|
|
341
|
+
await anomira.flush();
|
|
342
|
+
});
|
|
343
|
+
```
|
|
217
344
|
|
|
218
|
-
|
|
345
|
+
---
|
|
219
346
|
|
|
220
|
-
|
|
347
|
+
## Environment variables reference
|
|
221
348
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
349
|
+
| Variable | Required | Description |
|
|
350
|
+
|---|---|---|
|
|
351
|
+
| `ANOMIRA_API_KEY` | ✅ | Your API key — from dashboard under **Apps → Setup** |
|
|
352
|
+
| `ANOMIRA_APP_ID` | ✅ | Your app ID — from dashboard under **Apps → Setup** |
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Configuration reference
|
|
357
|
+
|
|
358
|
+
| Option | Type | Default | Description |
|
|
359
|
+
|---|---|---|---|
|
|
360
|
+
| `apiKey` | `string` | — | API key (required) |
|
|
361
|
+
| `appId` | `string` | — | App ID (required) |
|
|
362
|
+
| `debug` | `boolean` | `false` | Log SDK activity to console |
|
|
363
|
+
| `service` | `string` | `"app"` | Service name tag on all log entries |
|
|
364
|
+
| `captureConsole` | `boolean` | `false` | Forward `console.*` to Logs dashboard |
|
|
365
|
+
| `detect.bruteForce` | `boolean` | `true` | Brute force detection on auth endpoints |
|
|
366
|
+
| `detect.rateAbuse` | `boolean` | `true` | High-rate abuse from single IP |
|
|
367
|
+
| `detect.pathTraversal` | `boolean` | `true` | Path traversal payloads in URLs |
|
|
368
|
+
| `detect.xss` | `boolean` | `true` | XSS payloads in request bodies |
|
|
369
|
+
| `detect.scanDetection` | `boolean` | `true` | Endpoint scanner / bot probing |
|
|
370
|
+
| `detect.geoVelocity` | `boolean` | `true` | Impossible travel between logins |
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Troubleshooting
|
|
375
|
+
|
|
376
|
+
**No events showing in the dashboard?**
|
|
377
|
+
1. Set `debug: true` in the constructor — the SDK will log every event it sends to the console.
|
|
378
|
+
2. Check that `ANOMIRA_API_KEY` and `ANOMIRA_APP_ID` are set in your process environment (`console.log(process.env.ANOMIRA_API_KEY)`).
|
|
379
|
+
3. Confirm the middleware is registered **before** your routes and **after** body parsers.
|
|
380
|
+
4. Make sure you're not blocking outbound HTTPS traffic to `api.anomira.io`.
|
|
381
|
+
|
|
382
|
+
**TypeScript errors on `req.ip`?**
|
|
383
|
+
Express types sometimes don't include `ip` directly. Use `(req as express.Request).ip ?? req.socket.remoteAddress ?? "0.0.0.0"`.
|
|
384
|
+
|
|
385
|
+
**`flush()` taking too long on shutdown?**
|
|
386
|
+
The flush waits for in-flight HTTP requests to complete. If your process needs to exit fast, you can add a timeout:
|
|
387
|
+
```ts
|
|
388
|
+
await Promise.race([
|
|
389
|
+
anomira.flush(),
|
|
390
|
+
new Promise((resolve) => setTimeout(resolve, 3000)),
|
|
391
|
+
]);
|
|
227
392
|
```
|
|
228
393
|
|
|
394
|
+
---
|
|
395
|
+
|
|
229
396
|
## License
|
|
230
397
|
|
|
231
398
|
MIT
|