safeprompt-middleware 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/src/next.ts ADDED
@@ -0,0 +1,132 @@
1
+ import type { GuardConfig, NextHandler } from './types.js';
2
+ import { validate, DEFAULT_PROVIDER } from './client.js';
3
+
4
+ function getClientIP(req: any): string | undefined {
5
+ const forwarded = req.headers?.['x-forwarded-for'];
6
+ if (forwarded) return String(forwarded).split(',')[0].trim();
7
+ return req.socket?.remoteAddress;
8
+ }
9
+
10
+ /**
11
+ * Wraps a Next.js Pages Router API handler with prompt injection protection.
12
+ * Validates req.body[fieldName] before passing to the original handler.
13
+ *
14
+ * @example — Pages Router
15
+ * export default withGuard(handler, { apiKey: process.env.GUARD_API_KEY })
16
+ *
17
+ * @example — App Router (Route Handler)
18
+ * export const POST = withGuardRoute(handler, { apiKey: process.env.GUARD_API_KEY })
19
+ */
20
+ export function withGuard(handler: NextHandler, config: GuardConfig): NextHandler {
21
+ const {
22
+ provider = DEFAULT_PROVIDER,
23
+ apiKey,
24
+ mode = 'balanced',
25
+ fieldName = 'prompt',
26
+ failOpen = false,
27
+ onBlock,
28
+ onError,
29
+ } = config;
30
+
31
+ if (!apiKey) throw new Error('ai-security-middleware: apiKey is required');
32
+
33
+ return async function guardedHandler(req: any, res: any) {
34
+ const input = req.body?.[fieldName];
35
+
36
+ if (input === undefined || input === null || typeof input !== 'string') {
37
+ return handler(req, res);
38
+ }
39
+
40
+ try {
41
+ const result = await validate(input, {
42
+ provider,
43
+ apiKey,
44
+ mode,
45
+ userIP: getClientIP(req),
46
+ });
47
+
48
+ if (!result.safe) {
49
+ if (onBlock) return onBlock(req, res, result);
50
+ return res.status(400).json({
51
+ error: 'Request blocked: prompt injection detected',
52
+ threats: result.threats,
53
+ confidence: result.confidence,
54
+ });
55
+ }
56
+
57
+ req.guardResult = result;
58
+ return handler(req, res);
59
+ } catch (err: any) {
60
+ if (onError) return onError(req, res, err);
61
+ if (failOpen) {
62
+ req.guardError = err;
63
+ return handler(req, res);
64
+ }
65
+ return res.status(500).json({ error: 'Security check unavailable' });
66
+ }
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Wraps a Next.js App Router (Route Handler) with prompt injection protection.
72
+ * Reads the JSON body and validates the specified field.
73
+ *
74
+ * @example
75
+ * export const POST = withGuardRoute(async (req) => {
76
+ * const { message } = await req.json()
77
+ * return Response.json({ reply: await chat(message) })
78
+ * }, { apiKey: process.env.GUARD_API_KEY, fieldName: 'message' })
79
+ */
80
+ export function withGuardRoute(
81
+ handler: (req: Request) => Promise<Response>,
82
+ config: GuardConfig,
83
+ ): (req: Request) => Promise<Response> {
84
+ const {
85
+ provider = DEFAULT_PROVIDER,
86
+ apiKey,
87
+ mode = 'balanced',
88
+ fieldName = 'prompt',
89
+ failOpen = false,
90
+ } = config;
91
+
92
+ if (!apiKey) throw new Error('ai-security-middleware: apiKey is required');
93
+
94
+ return async function guardedRouteHandler(req: Request): Promise<Response> {
95
+ let body: Record<string, any>;
96
+ try {
97
+ body = await req.clone().json();
98
+ } catch {
99
+ return handler(req);
100
+ }
101
+
102
+ const input = body[fieldName];
103
+ if (input === undefined || input === null || typeof input !== 'string') {
104
+ return handler(req);
105
+ }
106
+
107
+ const userIP = req.headers.get('x-forwarded-for')?.split(',')[0].trim() ?? undefined;
108
+
109
+ try {
110
+ const result = await validate(input, { provider, apiKey, mode, userIP });
111
+
112
+ if (!result.safe) {
113
+ return new Response(
114
+ JSON.stringify({
115
+ error: 'Request blocked: prompt injection detected',
116
+ threats: result.threats,
117
+ confidence: result.confidence,
118
+ }),
119
+ { status: 400, headers: { 'Content-Type': 'application/json' } },
120
+ );
121
+ }
122
+
123
+ return handler(req);
124
+ } catch (err) {
125
+ if (failOpen) return handler(req);
126
+ return new Response(
127
+ JSON.stringify({ error: 'Security check unavailable' }),
128
+ { status: 500, headers: { 'Content-Type': 'application/json' } },
129
+ );
130
+ }
131
+ };
132
+ }
package/src/types.ts ADDED
@@ -0,0 +1,48 @@
1
+ export interface ValidationResult {
2
+ safe: boolean;
3
+ threats: string[];
4
+ confidence: number;
5
+ processingTimeMs: number;
6
+ passesUsed: number;
7
+ request_id?: string;
8
+ timestamp?: string;
9
+ }
10
+
11
+ export interface GuardConfig {
12
+ /**
13
+ * Provider base URL. Must be a conformant ai-security-gateway-spec endpoint.
14
+ * Defaults to SafePrompt reference implementation.
15
+ */
16
+ provider?: string;
17
+ /** API key for the provider. */
18
+ apiKey: string;
19
+ /**
20
+ * Detection sensitivity mode. Defaults to 'balanced'.
21
+ * - 'fast': pattern detection only, <5ms
22
+ * - 'balanced': pattern + AI pass 1 when needed (default)
23
+ * - 'strict': always uses full AI validation
24
+ */
25
+ mode?: 'fast' | 'balanced' | 'strict';
26
+ /**
27
+ * The req.body field to validate. Defaults to 'prompt'.
28
+ * Set to 'message' for chat-style APIs, 'input' for others.
29
+ */
30
+ fieldName?: string;
31
+ /**
32
+ * If the provider is unreachable, allow requests through (fail-open).
33
+ * Defaults to false (fail-closed — block on provider error).
34
+ */
35
+ failOpen?: boolean;
36
+ /**
37
+ * Called when a prompt is blocked. Default behaviour: 400 with threats array.
38
+ */
39
+ onBlock?: (req: any, res: any, result: ValidationResult) => void | Promise<void>;
40
+ /**
41
+ * Called when the provider returns an error or is unreachable.
42
+ * Default: fail-closed (500) or fail-open based on failOpen setting.
43
+ */
44
+ onError?: (req: any, res: any, error: Error) => void | Promise<void>;
45
+ }
46
+
47
+ export type NextHandler = (req: any, res: any) => any;
48
+ export type ExpressMiddleware = (req: any, res: any, next: () => void) => void;