guardrail-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/collector.d.ts +21 -0
- package/dist/collector.js +87 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +99 -0
- package/dist/next.d.ts +25 -0
- package/dist/next.js +110 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.js +1 -0
- package/package.json +21 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SdkEvent, ResolvedConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Collects SDK events in-memory and sends them in batches to the GuardRail API.
|
|
4
|
+
* Also tracks blocked IPs returned by the server.
|
|
5
|
+
*/
|
|
6
|
+
export declare class EventCollector {
|
|
7
|
+
private config;
|
|
8
|
+
private queue;
|
|
9
|
+
private blockedIps;
|
|
10
|
+
private timer;
|
|
11
|
+
private flushing;
|
|
12
|
+
constructor(config: ResolvedConfig);
|
|
13
|
+
/** Add an event to the queue. Triggers immediate flush if batch is full. */
|
|
14
|
+
push(event: SdkEvent): void;
|
|
15
|
+
/** Check if an IP is in the blocked list. */
|
|
16
|
+
isBlocked(ip: string): boolean;
|
|
17
|
+
/** Send queued events to the GuardRail API. */
|
|
18
|
+
flush(): Promise<void>;
|
|
19
|
+
/** Stop the collector and flush remaining events. */
|
|
20
|
+
destroy(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects SDK events in-memory and sends them in batches to the GuardRail API.
|
|
3
|
+
* Also tracks blocked IPs returned by the server.
|
|
4
|
+
*/
|
|
5
|
+
export class EventCollector {
|
|
6
|
+
config;
|
|
7
|
+
queue = [];
|
|
8
|
+
blockedIps = new Set();
|
|
9
|
+
timer = null;
|
|
10
|
+
flushing = false;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
// Start periodic flush
|
|
14
|
+
this.timer = setInterval(() => {
|
|
15
|
+
void this.flush();
|
|
16
|
+
}, this.config.flushIntervalMs);
|
|
17
|
+
// Ensure timer doesn't prevent process exit
|
|
18
|
+
if (this.timer && typeof this.timer === "object" && "unref" in this.timer) {
|
|
19
|
+
this.timer.unref();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Add an event to the queue. Triggers immediate flush if batch is full. */
|
|
23
|
+
push(event) {
|
|
24
|
+
this.queue.push(event);
|
|
25
|
+
if (this.queue.length >= this.config.maxBatchSize) {
|
|
26
|
+
void this.flush();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Check if an IP is in the blocked list. */
|
|
30
|
+
isBlocked(ip) {
|
|
31
|
+
return this.blockedIps.has(ip);
|
|
32
|
+
}
|
|
33
|
+
/** Send queued events to the GuardRail API. */
|
|
34
|
+
async flush() {
|
|
35
|
+
if (this.flushing || this.queue.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
this.flushing = true;
|
|
38
|
+
const batch = this.queue.splice(0, this.config.maxBatchSize);
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`${this.config.apiUrl}/api/events`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
Authorization: `Bearer ${this.config.projectKey}`,
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({ events: batch }),
|
|
47
|
+
});
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
const data = (await res.json());
|
|
50
|
+
// Update blocked IPs from server response
|
|
51
|
+
if (data.blockedIps && Array.isArray(data.blockedIps)) {
|
|
52
|
+
for (const entry of data.blockedIps) {
|
|
53
|
+
this.blockedIps.add(entry.ip);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (this.config.debug) {
|
|
57
|
+
console.error(`[guardrail-sdk] Flushed ${batch.length} events. Blocked IPs: ${this.blockedIps.size}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// API error — put events back in queue for retry
|
|
62
|
+
this.queue.unshift(...batch);
|
|
63
|
+
if (this.config.debug) {
|
|
64
|
+
console.error(`[guardrail-sdk] Flush failed (${res.status}). ${batch.length} events re-queued.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// Network error — put events back in queue for retry
|
|
70
|
+
this.queue.unshift(...batch);
|
|
71
|
+
if (this.config.debug) {
|
|
72
|
+
console.error(`[guardrail-sdk] Flush error: ${err instanceof Error ? err.message : "Unknown error"}. ${batch.length} events re-queued.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
this.flushing = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Stop the collector and flush remaining events. */
|
|
80
|
+
async destroy() {
|
|
81
|
+
if (this.timer) {
|
|
82
|
+
clearInterval(this.timer);
|
|
83
|
+
this.timer = null;
|
|
84
|
+
}
|
|
85
|
+
await this.flush();
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GuardrailConfig } from "./types.js";
|
|
2
|
+
export type { GuardrailConfig, SdkEvent, SdkEventType } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Express/Connect middleware for GuardRail runtime monitoring.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { guardrail } from 'guardrail-sdk'
|
|
9
|
+
*
|
|
10
|
+
* app.use(guardrail({
|
|
11
|
+
* projectKey: process.env.GUARDRAIL_PROJECT_KEY
|
|
12
|
+
* }))
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function guardrail(config: GuardrailConfig): (req: {
|
|
16
|
+
ip?: string;
|
|
17
|
+
socket?: {
|
|
18
|
+
remoteAddress?: string;
|
|
19
|
+
};
|
|
20
|
+
headers: Record<string, string | string[] | undefined>;
|
|
21
|
+
method?: string;
|
|
22
|
+
path?: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
}, res: {
|
|
25
|
+
statusCode: number;
|
|
26
|
+
status: (code: number) => {
|
|
27
|
+
json: (body: unknown) => void;
|
|
28
|
+
};
|
|
29
|
+
end: (...args: unknown[]) => unknown;
|
|
30
|
+
}, next: () => void) => void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { EventCollector } from "./collector.js";
|
|
2
|
+
const DEFAULT_API_URL = "https://guardrail-seven.vercel.app";
|
|
3
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 30_000;
|
|
4
|
+
const DEFAULT_MAX_BATCH_SIZE = 50;
|
|
5
|
+
/** Auth endpoint patterns */
|
|
6
|
+
const AUTH_PATTERNS = [
|
|
7
|
+
/\/login/i,
|
|
8
|
+
/\/signin/i,
|
|
9
|
+
/\/sign-in/i,
|
|
10
|
+
/\/auth/i,
|
|
11
|
+
/\/api\/auth/i,
|
|
12
|
+
/\/api\/login/i,
|
|
13
|
+
/\/session/i,
|
|
14
|
+
];
|
|
15
|
+
function isAuthEndpoint(path) {
|
|
16
|
+
return AUTH_PATTERNS.some((p) => p.test(path));
|
|
17
|
+
}
|
|
18
|
+
function resolveConfig(config) {
|
|
19
|
+
return {
|
|
20
|
+
projectKey: config.projectKey,
|
|
21
|
+
apiUrl: config.apiUrl || DEFAULT_API_URL,
|
|
22
|
+
flushIntervalMs: config.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS,
|
|
23
|
+
maxBatchSize: config.maxBatchSize || DEFAULT_MAX_BATCH_SIZE,
|
|
24
|
+
debug: config.debug || false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Express/Connect middleware for GuardRail runtime monitoring.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { guardrail } from 'guardrail-sdk'
|
|
33
|
+
*
|
|
34
|
+
* app.use(guardrail({
|
|
35
|
+
* projectKey: process.env.GUARDRAIL_PROJECT_KEY
|
|
36
|
+
* }))
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function guardrail(config) {
|
|
40
|
+
if (!config.projectKey) {
|
|
41
|
+
console.error("[guardrail-sdk] Missing projectKey. Set GUARDRAIL_PROJECT_KEY environment variable.");
|
|
42
|
+
// Return no-op middleware
|
|
43
|
+
return function guardrailNoOp(_req, _res, next) {
|
|
44
|
+
next();
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const resolved = resolveConfig(config);
|
|
48
|
+
const collector = new EventCollector(resolved);
|
|
49
|
+
if (resolved.debug) {
|
|
50
|
+
console.error("[guardrail-sdk] Initialized with Express middleware");
|
|
51
|
+
}
|
|
52
|
+
// Express middleware signature
|
|
53
|
+
return function guardrailMiddleware(req, res, next) {
|
|
54
|
+
const ip = req.ip || req.socket?.remoteAddress || "";
|
|
55
|
+
const userAgent = req.headers["user-agent"] || "";
|
|
56
|
+
const path = req.path || req.url || "";
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
// Block denied IPs
|
|
59
|
+
if (collector.isBlocked(ip)) {
|
|
60
|
+
res.status(403).json({ error: "Access denied" });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Track auth endpoint responses
|
|
64
|
+
if (isAuthEndpoint(path) && req.method === "POST") {
|
|
65
|
+
const origEnd = res.end;
|
|
66
|
+
res.end = function (...args) {
|
|
67
|
+
const status = res.statusCode;
|
|
68
|
+
if (status === 401 || status === 403) {
|
|
69
|
+
collector.push({
|
|
70
|
+
type: "auth.login_failed",
|
|
71
|
+
timestamp: now,
|
|
72
|
+
ip,
|
|
73
|
+
userAgent,
|
|
74
|
+
metadata: { path, method: req.method },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else if (status >= 200 && status < 300) {
|
|
78
|
+
collector.push({
|
|
79
|
+
type: "auth.login_success",
|
|
80
|
+
timestamp: now,
|
|
81
|
+
ip,
|
|
82
|
+
userAgent,
|
|
83
|
+
metadata: { path, method: req.method },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return origEnd.apply(res, args);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Track all API requests
|
|
90
|
+
collector.push({
|
|
91
|
+
type: "api.request",
|
|
92
|
+
timestamp: now,
|
|
93
|
+
ip,
|
|
94
|
+
userAgent,
|
|
95
|
+
metadata: { path, method: req.method },
|
|
96
|
+
});
|
|
97
|
+
next();
|
|
98
|
+
};
|
|
99
|
+
}
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { GuardrailConfig } from "./types.js";
|
|
2
|
+
export type { GuardrailConfig, SdkEvent, SdkEventType } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Next.js middleware for GuardRail runtime monitoring.
|
|
5
|
+
*
|
|
6
|
+
* Usage in middleware.ts:
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { guardrailMiddleware } from 'guardrail-sdk/next'
|
|
9
|
+
*
|
|
10
|
+
* const guardrail = guardrailMiddleware({
|
|
11
|
+
* projectKey: process.env.GUARDRAIL_PROJECT_KEY!,
|
|
12
|
+
* })
|
|
13
|
+
*
|
|
14
|
+
* export async function middleware(request: Request) {
|
|
15
|
+
* const blocked = guardrail(request)
|
|
16
|
+
* if (blocked) return blocked
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Note: Next.js middleware cannot access response status codes,
|
|
21
|
+
* so auth.login_failed/success events cannot be auto-detected.
|
|
22
|
+
* For auth event tracking, call the GuardRail API directly from
|
|
23
|
+
* your API route handlers.
|
|
24
|
+
*/
|
|
25
|
+
export declare function guardrailMiddleware(config: GuardrailConfig): (request: Request) => Response | null;
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { EventCollector } from "./collector.js";
|
|
2
|
+
const DEFAULT_API_URL = "https://guardrail-seven.vercel.app";
|
|
3
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 30_000;
|
|
4
|
+
const DEFAULT_MAX_BATCH_SIZE = 50;
|
|
5
|
+
const AUTH_PATTERNS = [
|
|
6
|
+
/\/login/i,
|
|
7
|
+
/\/signin/i,
|
|
8
|
+
/\/sign-in/i,
|
|
9
|
+
/\/auth/i,
|
|
10
|
+
/\/api\/auth/i,
|
|
11
|
+
/\/api\/login/i,
|
|
12
|
+
/\/session/i,
|
|
13
|
+
];
|
|
14
|
+
function isAuthEndpoint(path) {
|
|
15
|
+
return AUTH_PATTERNS.some((p) => p.test(path));
|
|
16
|
+
}
|
|
17
|
+
function resolveConfig(config) {
|
|
18
|
+
return {
|
|
19
|
+
projectKey: config.projectKey,
|
|
20
|
+
apiUrl: config.apiUrl || DEFAULT_API_URL,
|
|
21
|
+
flushIntervalMs: config.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS,
|
|
22
|
+
maxBatchSize: config.maxBatchSize || DEFAULT_MAX_BATCH_SIZE,
|
|
23
|
+
debug: config.debug || false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// Singleton collector (Next.js middleware runs in edge runtime, persists across requests)
|
|
27
|
+
let collector = null;
|
|
28
|
+
function getCollector(config) {
|
|
29
|
+
if (!collector) {
|
|
30
|
+
collector = new EventCollector(config);
|
|
31
|
+
}
|
|
32
|
+
return collector;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Next.js middleware for GuardRail runtime monitoring.
|
|
36
|
+
*
|
|
37
|
+
* Usage in middleware.ts:
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { guardrailMiddleware } from 'guardrail-sdk/next'
|
|
40
|
+
*
|
|
41
|
+
* const guardrail = guardrailMiddleware({
|
|
42
|
+
* projectKey: process.env.GUARDRAIL_PROJECT_KEY!,
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* export async function middleware(request: Request) {
|
|
46
|
+
* const blocked = guardrail(request)
|
|
47
|
+
* if (blocked) return blocked
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* Note: Next.js middleware cannot access response status codes,
|
|
52
|
+
* so auth.login_failed/success events cannot be auto-detected.
|
|
53
|
+
* For auth event tracking, call the GuardRail API directly from
|
|
54
|
+
* your API route handlers.
|
|
55
|
+
*/
|
|
56
|
+
export function guardrailMiddleware(config) {
|
|
57
|
+
if (!config.projectKey) {
|
|
58
|
+
console.error("[guardrail-sdk] Missing projectKey. Set GUARDRAIL_PROJECT_KEY environment variable.");
|
|
59
|
+
return function guardrailNoOp() {
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const resolved = resolveConfig(config);
|
|
64
|
+
if (resolved.debug) {
|
|
65
|
+
console.error("[guardrail-sdk] Initialized with Next.js middleware");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Call this function inside your Next.js middleware.
|
|
69
|
+
* Returns a Response if the IP is blocked, or null to continue.
|
|
70
|
+
*/
|
|
71
|
+
return function guardrail(request) {
|
|
72
|
+
const c = getCollector(resolved);
|
|
73
|
+
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
|
74
|
+
request.headers.get("x-real-ip") ||
|
|
75
|
+
"";
|
|
76
|
+
const userAgent = request.headers.get("user-agent") || "";
|
|
77
|
+
let pathname;
|
|
78
|
+
try {
|
|
79
|
+
pathname = new URL(request.url).pathname;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
pathname = request.url;
|
|
83
|
+
}
|
|
84
|
+
// Block denied IPs
|
|
85
|
+
if (c.isBlocked(ip)) {
|
|
86
|
+
return new Response(JSON.stringify({ error: "Access denied" }), {
|
|
87
|
+
status: 403,
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const now = new Date().toISOString();
|
|
92
|
+
// Track API requests
|
|
93
|
+
c.push({
|
|
94
|
+
type: "api.request",
|
|
95
|
+
timestamp: now,
|
|
96
|
+
ip,
|
|
97
|
+
userAgent,
|
|
98
|
+
metadata: { path: pathname, method: request.method },
|
|
99
|
+
});
|
|
100
|
+
// Track auth endpoints (request-only, since we can't see response status)
|
|
101
|
+
if (isAuthEndpoint(pathname) && request.method === "POST") {
|
|
102
|
+
// We log as a generic auth attempt — the server-side analyzer
|
|
103
|
+
// will use the api.request pattern combined with other signals
|
|
104
|
+
if (resolved.debug) {
|
|
105
|
+
console.error(`[guardrail-sdk] Auth endpoint hit: ${pathname} from ${ip}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null; // Continue to next middleware
|
|
109
|
+
};
|
|
110
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type SdkEventType = "auth.login_failed" | "auth.login_success" | "api.request";
|
|
2
|
+
export interface SdkEvent {
|
|
3
|
+
type: SdkEventType;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
ip: string;
|
|
6
|
+
userAgent: string;
|
|
7
|
+
metadata: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface GuardrailConfig {
|
|
10
|
+
/** Project key from GuardRail dashboard (gr_sk_...) */
|
|
11
|
+
projectKey: string;
|
|
12
|
+
/** API URL override (default: https://guardrail-seven.vercel.app) */
|
|
13
|
+
apiUrl?: string;
|
|
14
|
+
/** Batch flush interval in ms (default: 30000) */
|
|
15
|
+
flushIntervalMs?: number;
|
|
16
|
+
/** Max events per batch (default: 50) */
|
|
17
|
+
maxBatchSize?: number;
|
|
18
|
+
/** Enable debug logging to stderr (default: false) */
|
|
19
|
+
debug?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ResolvedConfig {
|
|
22
|
+
projectKey: string;
|
|
23
|
+
apiUrl: string;
|
|
24
|
+
flushIntervalMs: number;
|
|
25
|
+
maxBatchSize: number;
|
|
26
|
+
debug: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface BlockedIpEntry {
|
|
29
|
+
ip: string;
|
|
30
|
+
blockedAt: string;
|
|
31
|
+
}
|
|
32
|
+
export interface FlushResponse {
|
|
33
|
+
received: number;
|
|
34
|
+
blockedIps: BlockedIpEntry[];
|
|
35
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "guardrail-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "GuardRail runtime security monitoring SDK — detect brute force, suspicious logins, and traffic spikes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/index.js",
|
|
8
|
+
"./next": "./dist/next.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.0.0",
|
|
19
|
+
"typescript": "^5.7.0"
|
|
20
|
+
}
|
|
21
|
+
}
|