@anomira/node-sdk 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/dist/index.cjs +973 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +315 -0
- package/dist/index.d.ts +315 -0
- package/dist/index.js +965 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { FastifyPluginAsync } from 'fastify';
|
|
3
|
+
|
|
4
|
+
interface SentinelConfig {
|
|
5
|
+
/** SDK API key — get this from the SentinelAPI dashboard */
|
|
6
|
+
apiKey: string;
|
|
7
|
+
/** Your app ID from the SentinelAPI dashboard */
|
|
8
|
+
appId: string;
|
|
9
|
+
/**
|
|
10
|
+
* Ingest endpoint URL.
|
|
11
|
+
* @default "https://ingest.anomira.io/v1/events"
|
|
12
|
+
*/
|
|
13
|
+
ingestUrl?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Geo-lookup endpoint URL for client-side geo-velocity checks.
|
|
16
|
+
* Should point to your SentinelAPI ingest service geo endpoint.
|
|
17
|
+
* If not provided, client-side geo-velocity is skipped (server-side still runs).
|
|
18
|
+
* @example "https://ingest.anomira.io/v1/geo"
|
|
19
|
+
*/
|
|
20
|
+
geoLookupUrl?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Max events to buffer before forcing a flush.
|
|
23
|
+
* @default 100
|
|
24
|
+
*/
|
|
25
|
+
maxBatchSize?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Max milliseconds to hold events before flushing.
|
|
28
|
+
* @default 5000
|
|
29
|
+
*/
|
|
30
|
+
flushIntervalMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Max retry attempts on ingest failure.
|
|
33
|
+
* @default 3
|
|
34
|
+
*/
|
|
35
|
+
maxRetries?: number;
|
|
36
|
+
/**
|
|
37
|
+
* If true, log SDK activity to console (useful during integration).
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
debug?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* If true, automatically intercept console.log/info/warn/error/debug
|
|
43
|
+
* and forward them to the SentinelAPI Logs dashboard.
|
|
44
|
+
* Existing console output is preserved — logs still print to your terminal.
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
captureConsole?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Service name tag applied to all captured console logs.
|
|
50
|
+
* @default "app"
|
|
51
|
+
*/
|
|
52
|
+
service?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Auto-detection features to enable in the middleware.
|
|
55
|
+
* All default to true.
|
|
56
|
+
*/
|
|
57
|
+
detect?: {
|
|
58
|
+
/** Flag repeated login failures from the same IP as brute_force */
|
|
59
|
+
bruteForce?: boolean;
|
|
60
|
+
/** Flag 429 responses as rate_abuse */
|
|
61
|
+
rateAbuse?: boolean;
|
|
62
|
+
/** Flag path traversal patterns in the URL */
|
|
63
|
+
pathTraversal?: boolean;
|
|
64
|
+
/** Flag XSS patterns in request body */
|
|
65
|
+
xss?: boolean;
|
|
66
|
+
/** Flag suspicious scan patterns (many 404s) */
|
|
67
|
+
scanDetection?: boolean;
|
|
68
|
+
/** Detect impossible travel between login events */
|
|
69
|
+
geoVelocity?: boolean;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Custom function to extract userId from the request.
|
|
73
|
+
* Default: reads req.user?.id || req.user?.userId || req.user?.sub
|
|
74
|
+
*/
|
|
75
|
+
getUserId?: (req: unknown) => string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Custom function to extract the client IP from the request.
|
|
78
|
+
* Default: reads X-Forwarded-For → X-Real-IP → socket.remoteAddress
|
|
79
|
+
*/
|
|
80
|
+
getIp?: (req: unknown) => string;
|
|
81
|
+
}
|
|
82
|
+
interface SdkEvent {
|
|
83
|
+
name: string;
|
|
84
|
+
ts: number;
|
|
85
|
+
ip: string;
|
|
86
|
+
userId?: string;
|
|
87
|
+
meta?: Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
interface IngestPayload {
|
|
90
|
+
appId: string;
|
|
91
|
+
events: SdkEvent[];
|
|
92
|
+
}
|
|
93
|
+
declare const EventName: {
|
|
94
|
+
readonly LOGIN_SUCCESS: "auth.login.success";
|
|
95
|
+
readonly LOGIN_FAILED: "auth.login.failed";
|
|
96
|
+
readonly LOGOUT: "auth.logout";
|
|
97
|
+
readonly OTP_FAILED: "auth.otp.failed";
|
|
98
|
+
readonly OTP_SUCCESS: "auth.otp.success";
|
|
99
|
+
readonly BVN_LOOKUP: "auth.bvn.lookup";
|
|
100
|
+
readonly NIN_LOOKUP: "auth.nin.lookup";
|
|
101
|
+
readonly GEO_VELOCITY: "auth.login.geo_velocity";
|
|
102
|
+
readonly CREDENTIAL_STUFF: "auth.credential.stuffing";
|
|
103
|
+
readonly SIM_SWAP: "auth.sim_swap.suspected";
|
|
104
|
+
readonly PHONE_AUTH: "auth.phone.verified";
|
|
105
|
+
readonly REQUEST: "http.request";
|
|
106
|
+
readonly RATE_LIMIT: "http.ratelimit.exceeded";
|
|
107
|
+
readonly XSS_DETECTED: "http.xss.detected";
|
|
108
|
+
readonly PATH_TRAVERSAL: "http.path.traversal";
|
|
109
|
+
readonly SCAN_DETECTED: "http.scan.detected";
|
|
110
|
+
readonly IDOR_ATTEMPT: "user.idor.attempt";
|
|
111
|
+
readonly SQL_ERROR: "db.sql.error";
|
|
112
|
+
readonly FIREWALL_BLOCK: "http.firewall.block";
|
|
113
|
+
readonly FIREWALL_FLAG: "http.firewall.flag";
|
|
114
|
+
};
|
|
115
|
+
type EventNameValue = typeof EventName[keyof typeof EventName];
|
|
116
|
+
type FirewallField = "url" | "body" | "header" | "user_agent" | "ip";
|
|
117
|
+
type FirewallOperator = "contains" | "equals" | "starts_with" | "ends_with" | "regex";
|
|
118
|
+
type FirewallAction = "block" | "flag";
|
|
119
|
+
interface FirewallRule {
|
|
120
|
+
id: string;
|
|
121
|
+
field: FirewallField;
|
|
122
|
+
headerName?: string | null;
|
|
123
|
+
operator: FirewallOperator;
|
|
124
|
+
value: string;
|
|
125
|
+
action: FirewallAction;
|
|
126
|
+
attackType: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* SentinelClient — the core SDK object.
|
|
131
|
+
*
|
|
132
|
+
* Instantiate once and reuse across your application:
|
|
133
|
+
*
|
|
134
|
+
* ```ts
|
|
135
|
+
* import { SentinelAPI } from "@anomira/node-sdk";
|
|
136
|
+
*
|
|
137
|
+
* export const sentinel = new SentinelAPI({
|
|
138
|
+
* apiKey: process.env.SENTINEL_API_KEY!,
|
|
139
|
+
* appId: process.env.SENTINEL_APP_ID!,
|
|
140
|
+
* });
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
declare class SentinelClient {
|
|
144
|
+
#private;
|
|
145
|
+
readonly config: Required<Omit<SentinelConfig, "getUserId" | "getIp" | "detect">> & {
|
|
146
|
+
getUserId: NonNullable<SentinelConfig["getUserId"]>;
|
|
147
|
+
getIp: NonNullable<SentinelConfig["getIp"]>;
|
|
148
|
+
detect: Required<NonNullable<SentinelConfig["detect"]>>;
|
|
149
|
+
captureConsole: boolean;
|
|
150
|
+
service: string;
|
|
151
|
+
};
|
|
152
|
+
private readonly buffer;
|
|
153
|
+
private readonly logBuffer;
|
|
154
|
+
private logFlushTimer;
|
|
155
|
+
private blocklistTimer;
|
|
156
|
+
private firewallTimer;
|
|
157
|
+
/** In-process cache of blocked IPs — refreshed every 60 s from the ingest server. */
|
|
158
|
+
private blockedIpCache;
|
|
159
|
+
/** In-process cache of firewall rules with pre-compiled regex — refreshed every 60 s. */
|
|
160
|
+
private compiledRules;
|
|
161
|
+
private readonly _origLog;
|
|
162
|
+
private readonly _origWarn;
|
|
163
|
+
private readonly _origError;
|
|
164
|
+
constructor(config: SentinelConfig);
|
|
165
|
+
/**
|
|
166
|
+
* Track a custom security event.
|
|
167
|
+
*
|
|
168
|
+
* ```ts
|
|
169
|
+
* // Track a failed OTP attempt
|
|
170
|
+
* sentinel.track(EventName.OTP_FAILED, {
|
|
171
|
+
* ip: req.ip,
|
|
172
|
+
* userId: req.body.phone,
|
|
173
|
+
* meta: { endpoint: "/api/verify-otp", attempts: 3 },
|
|
174
|
+
* });
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
/** Returns true if the IP is in the current blocked list. Synchronous — no network call. */
|
|
178
|
+
isBlocked(ip: string): boolean;
|
|
179
|
+
/** Evaluate firewall rules against a request. Returns the matched rule or null. Synchronous. */
|
|
180
|
+
matchFirewallRule(req: {
|
|
181
|
+
url: string;
|
|
182
|
+
body: unknown;
|
|
183
|
+
headers: Record<string, string | undefined>;
|
|
184
|
+
ip: string;
|
|
185
|
+
}): {
|
|
186
|
+
rule: FirewallRule;
|
|
187
|
+
} | null;
|
|
188
|
+
track(eventName: string, data: {
|
|
189
|
+
ip: string;
|
|
190
|
+
userId?: string;
|
|
191
|
+
meta?: Record<string, unknown>;
|
|
192
|
+
}): void;
|
|
193
|
+
/**
|
|
194
|
+
* Track a successful login AND run geo-velocity check.
|
|
195
|
+
* If impossible travel is detected, automatically fires an additional
|
|
196
|
+
* `auth.login.geo_velocity` event with full context.
|
|
197
|
+
*
|
|
198
|
+
* ```ts
|
|
199
|
+
* await sentinel.trackLogin({
|
|
200
|
+
* ip: req.ip,
|
|
201
|
+
* userId: user.id,
|
|
202
|
+
* });
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
trackLogin(data: {
|
|
206
|
+
ip: string;
|
|
207
|
+
userId: string;
|
|
208
|
+
meta?: Record<string, unknown>;
|
|
209
|
+
}): Promise<void>;
|
|
210
|
+
/**
|
|
211
|
+
* Track phone-based authentication (OTP via SMS, WhatsApp, or call).
|
|
212
|
+
* The ingest service uses this to detect SIM swap patterns:
|
|
213
|
+
* if the same userId authenticates via phone but then appears on a new
|
|
214
|
+
* device/IP shortly after, it's flagged as a suspected SIM swap.
|
|
215
|
+
*
|
|
216
|
+
* ```ts
|
|
217
|
+
* await sentinel.trackPhoneAuth({
|
|
218
|
+
* ip: req.ip,
|
|
219
|
+
* userId: user.id,
|
|
220
|
+
* phone: user.phoneNumber,
|
|
221
|
+
* });
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
trackPhoneAuth(data: {
|
|
225
|
+
ip: string;
|
|
226
|
+
userId: string;
|
|
227
|
+
phone: string;
|
|
228
|
+
meta?: Record<string, unknown>;
|
|
229
|
+
}): void;
|
|
230
|
+
/**
|
|
231
|
+
* Send a structured log entry to the SentinelAPI Logs dashboard.
|
|
232
|
+
*
|
|
233
|
+
* ```ts
|
|
234
|
+
* sentinel.log("info", "User registered", { userId: user.id });
|
|
235
|
+
* sentinel.log("warn", "Slow DB query detected", { queryMs: 1240 });
|
|
236
|
+
* sentinel.log("error", "Payment failed", { reason: err.message });
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
log(level: "debug" | "info" | "warn" | "error" | "fatal", message: string, meta?: Record<string, unknown> & {
|
|
240
|
+
service?: string;
|
|
241
|
+
}): void;
|
|
242
|
+
/**
|
|
243
|
+
* Flush all pending events immediately.
|
|
244
|
+
* Useful before a graceful shutdown outside of the process lifecycle hooks.
|
|
245
|
+
*/
|
|
246
|
+
flush(): Promise<void>;
|
|
247
|
+
/**
|
|
248
|
+
* Express middleware — auto-instruments all routes.
|
|
249
|
+
*
|
|
250
|
+
* ```ts
|
|
251
|
+
* app.use(sentinel.express());
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
express(): (req: Record<string, unknown>, res: Record<string, unknown>, next: () => void) => Promise<void>;
|
|
255
|
+
/**
|
|
256
|
+
* Fastify plugin — auto-instruments all routes.
|
|
257
|
+
*
|
|
258
|
+
* ```ts
|
|
259
|
+
* await app.register(sentinel.fastify());
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
fastify(): (fastify: {
|
|
263
|
+
addHook: (event: string, fn: (req: Record<string, unknown>, reply: Record<string, unknown>) => void) => void;
|
|
264
|
+
}) => Promise<void>;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Typed Express middleware — exported separately for projects that want
|
|
269
|
+
* to import the middleware type explicitly.
|
|
270
|
+
*
|
|
271
|
+
* Most users will use `sentinel.express()` instead of importing this directly.
|
|
272
|
+
*/
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Returns fully-typed Express middleware.
|
|
276
|
+
*
|
|
277
|
+
* ```ts
|
|
278
|
+
* import express from "express";
|
|
279
|
+
* import { SentinelAPI } from "@sentinelapi/node-sdk";
|
|
280
|
+
*
|
|
281
|
+
* const app = express();
|
|
282
|
+
* const sentinel = new SentinelAPI({ apiKey: "...", appId: "..." });
|
|
283
|
+
*
|
|
284
|
+
* app.use(sentinel.express()); // auto-instrument all routes
|
|
285
|
+
*
|
|
286
|
+
* // Or import the typed version directly:
|
|
287
|
+
* import { createExpressMiddleware } from "@sentinelapi/node-sdk/middleware/express";
|
|
288
|
+
* app.use(createExpressMiddleware(sentinel));
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
declare function createExpressMiddleware(client: SentinelClient): (req: Request, res: Response, next: NextFunction) => void;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Typed Fastify plugin — exported separately for projects that want
|
|
295
|
+
* to import the plugin type explicitly.
|
|
296
|
+
*
|
|
297
|
+
* Most users will use `sentinel.fastify()` instead of importing this directly.
|
|
298
|
+
*/
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Returns a Fastify plugin for auto-instrumentation.
|
|
302
|
+
*
|
|
303
|
+
* ```ts
|
|
304
|
+
* import Fastify from "fastify";
|
|
305
|
+
* import { SentinelAPI } from "@sentinelapi/node-sdk";
|
|
306
|
+
*
|
|
307
|
+
* const app = Fastify();
|
|
308
|
+
* const sentinel = new SentinelAPI({ apiKey: "...", appId: "..." });
|
|
309
|
+
*
|
|
310
|
+
* await app.register(sentinel.fastify());
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
declare function createFastifyPlugin(client: SentinelClient): FastifyPluginAsync;
|
|
314
|
+
|
|
315
|
+
export { EventName, type EventNameValue, type IngestPayload, type SdkEvent, SentinelClient as SentinelAPI, type SentinelConfig, createExpressMiddleware, createFastifyPlugin, SentinelClient as default };
|