@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 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
+ [![npm version](https://img.shields.io/npm/v/@anomira/node-sdk)](https://www.npmjs.com/package/@anomira/node-sdk)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
- ## Quick Start
18
+ **Requirements:** Node.js 18+, ESM or CommonJS.
12
19
 
13
- ```js
14
- import { Anomira } from "@anomira/node-sdk";
20
+ ---
15
21
 
16
- const sentinel = new Anomira({
17
- apiKey: process.env.SENTINEL_API_KEY,
18
- appId: process.env.SENTINEL_APP_ID,
19
- ingestUrl: process.env.SENTINEL_INGEST_URL,
20
- debug: true,
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
- ```js
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 sentinel = new Anomira({
33
- apiKey: process.env.SENTINEL_API_KEY,
34
- appId: process.env.SENTINEL_APP_ID,
35
- ingestUrl: process.env.SENTINEL_INGEST_URL,
36
- debug: true,
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(sentinel.express()); // auto-instruments all routes
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
- ```js
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 sentinel = new Anomira({
64
- apiKey: process.env.SENTINEL_API_KEY,
65
- appId: process.env.SENTINEL_APP_ID,
66
- ingestUrl: process.env.SENTINEL_INGEST_URL,
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
- await app.register(sentinel.fastify());
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
- app.listen({ port: 3000 });
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
- ## Manual Event Tracking
147
+ ---
75
148
 
76
- ```js
77
- // Track a failed OTP attempt
78
- sentinel.track("auth.otp.failed", {
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
- // Track a login and run geo-velocity check automatically
85
- await sentinel.trackLogin({ ip: req.ip, userId: user.id });
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
- // Track phone-based auth (SIM swap detection)
88
- sentinel.trackPhoneAuth({ ip: req.ip, userId: user.id, phone: user.phone });
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
- ## Structured Logging
210
+ ---
92
211
 
93
- ```js
94
- sentinel.log("info", "User registered", { userId: user.id });
95
- sentinel.log("warn", "Slow DB query", { queryMs: 1240 });
96
- sentinel.log("error", "Payment failed", { reason: err.message });
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
- ## Blocklist & Firewall
223
+ Or set `captureConsole: true` in the constructor to automatically forward all `console.*` calls — no code changes needed.
100
224
 
101
- The SDK syncs your blocklist and firewall rules from the dashboard every 60 seconds. Check them in your own middleware:
225
+ ---
102
226
 
103
- ```js
104
- if (sentinel.isBlocked(req.ip)) {
105
- return res.status(403).json({ error: "Forbidden" });
106
- }
227
+ ## Blocklist & firewall check
107
228
 
108
- const match = sentinel.matchFirewallRule({
109
- url: req.url,
110
- body: req.body,
111
- headers: req.headers,
112
- ip: req.ip,
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
- ## Shadow Endpoint Detection
258
+ ---
120
259
 
121
- Register your known API routes on startup so Anomira can flag any undeclared endpoint that appears in live traffic. Endpoints that receive requests but were never declared show up in the **API Surface Map** as shadow endpoints.
260
+ ## Shadow endpoint detection
122
261
 
123
- ```js
124
- // Call once after your routes are registered
125
- await sentinel.declareEndpoints([
126
- { method: "POST", path: "/api/auth/login", auth: false },
127
- { method: "POST", path: "/api/auth/register", auth: false },
128
- { method: "GET", path: "/api/users/:id", auth: true },
129
- { method: "GET", path: "/api/orders", auth: true },
130
- { method: "POST", path: "/api/orders", auth: true },
131
- { method: "GET", path: "/api/health", auth: false },
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 `/users/:id` and `/users/:userId` both map to `/users/{id}` to match discovered traffic.
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
- Use `auth: false` for public endpoints. Authenticated endpoints default to `auth: true`.
280
+ ---
138
281
 
139
- Shadow detection from declared endpoints only activates once your org has registered at least one endpoint, so it won't produce noise on fresh deployments before you call this method.
282
+ ## Secret scanner CLI
140
283
 
141
- ## Secret Scanner CLI
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
- **If `@anomira/node-sdk` is already installed in your project:**
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 # enable entropy analysis (catches unknown secrets)
165
- npx @anomira/node-sdk scan ./src --json # machine-readable JSON output for CI
166
- npx @anomira/node-sdk scan ./src --quiet # only print violations, no header
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
- **What it detects:**
304
+ **Detects:**
170
305
 
171
306
  | Category | Examples |
172
307
  |---|---|
173
- | Cloud credentials | AWS access keys, GCP service account keys, Azure connection strings |
174
- | Source control | GitHub tokens (`ghp_`), GitLab tokens (`glpat-`), NPM tokens (`npm_`) |
175
- | Payment | Stripe keys (`sk_live_`), Paystack keys |
176
- | Communication | Slack tokens (`xoxb-`), Twilio credentials, SendGrid keys |
177
- | Database | Connection strings with embedded passwords (`postgresql://user:pass@host`) |
178
- | Auth | JWT tokens, generic API keys and bearer tokens |
179
- | Nigerian PII | BVN/NIN (11-digit), card PANs, Nigerian phone numbers |
180
- | Unknown secrets | High-entropy strings assigned to secret-like variables (`--strict`) |
181
-
182
- **Add to `package.json` for CI/CD:**
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
- Exit code `0` = clean. Exit code `1` = violations found — use in CI to fail the build on leaked secrets.
327
+ ---
192
328
 
193
- ## Environment Variables
329
+ ## Graceful shutdown
194
330
 
195
- | Variable | Description |
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
- ## Configuration
333
+ ```ts
334
+ process.on("SIGTERM", async () => {
335
+ await anomira.flush();
336
+ process.exit(0);
337
+ });
202
338
 
203
- | Option | Type | Default | Description |
204
- |---|---|---|---|
205
- | `apiKey` | `string` | — | Your Anomira API key (required) |
206
- | `appId` | `string` | — | Your Anomira app ID (required) |
207
- | `ingestUrl` | `string` | Anomira cloud | Ingest endpoint URL |
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
- ## Graceful Shutdown
345
+ ---
219
346
 
220
- Always flush pending events before your process exits:
347
+ ## Environment variables reference
221
348
 
222
- ```js
223
- process.on("SIGTERM", async () => {
224
- await sentinel.flush();
225
- process.exit(0);
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