secure-vibe-runtime 0.0.0-stage → 7.3.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.
@@ -0,0 +1,104 @@
1
+ import { NextResponse } from "next/server";
2
+ import { validateCsrfToken } from "./csrf";
3
+ import { ConvexHttpClient } from 'convex/browser';
4
+ import { makeFunctionReference } from 'convex/server';
5
+ /**
6
+ * The app's public security-log mutation. Named rather than imported from the
7
+ * app's generated api so this file can live in a package; the app registers
8
+ * `logSecurityViolation` in convex/security.ts with exactly these args.
9
+ */
10
+ const logSecurityViolation = makeFunctionReference('security:logSecurityViolation');
11
+ // Lazy-initialized Convex client for security logging
12
+ let convexClient = null;
13
+ function getConvexClient() {
14
+ if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
15
+ return null;
16
+ }
17
+ if (!convexClient) {
18
+ convexClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL);
19
+ }
20
+ return convexClient;
21
+ }
22
+ /**
23
+ * Log a CSRF violation to the security dashboard (non-blocking)
24
+ */
25
+ function logCsrfViolation(request, reason) {
26
+ const ip = request.headers.get('x-forwarded-for')?.split(',')[0].trim() ||
27
+ request.headers.get('x-real-ip') ||
28
+ 'unknown';
29
+ const endpoint = request.nextUrl.pathname;
30
+ const origin = request.headers.get('origin') || undefined;
31
+ // The mutation requires the shared SECURITY_LOG_KEY so that only this server,
32
+ // not anyone holding the public deployment URL, can write to the security log.
33
+ const client = getConvexClient();
34
+ const key = process.env.SECURITY_LOG_KEY;
35
+ if (client && !key) {
36
+ console.error('SECURITY_LOG_KEY is not set; CSRF violation not logged to Convex');
37
+ }
38
+ if (client && key) {
39
+ client.mutation(logSecurityViolation, {
40
+ key,
41
+ eventType: 'csrf_validation_failed',
42
+ severity: 'critical',
43
+ metadata: {
44
+ endpoint,
45
+ ipAddress: ip,
46
+ origin,
47
+ errorMessage: reason,
48
+ },
49
+ }).catch((logErr) => {
50
+ console.error('Failed to log CSRF violation:', logErr);
51
+ });
52
+ }
53
+ }
54
+ /**
55
+ * Higher-order function that wraps API route handlers with CSRF protection
56
+ * Validates the CSRF token from the request header against the stored cookie
57
+ * Tokens are single-use and cleared after validation
58
+ *
59
+ * @param handler - The API route handler to wrap
60
+ * @returns A wrapped handler with CSRF protection
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * import { withCsrf } from '@/lib/withCsrf';
65
+ *
66
+ * async function loginHandler(request: NextRequest) {
67
+ * // Your login logic here
68
+ * return NextResponse.json({ success: true });
69
+ * }
70
+ *
71
+ * export const POST = withCsrf(loginHandler);
72
+ * ```
73
+ */
74
+ export function withCsrf(handler) {
75
+ return async (request) => {
76
+ try {
77
+ const csrfToken = request.headers.get("x-csrf-token");
78
+ const storedToken = request.cookies.get("XSRF-TOKEN")?.value;
79
+ if (!csrfToken || !storedToken) {
80
+ const reason = !csrfToken ? "Missing CSRF token in header" : "Missing CSRF cookie";
81
+ console.error("CSRF check failed: Missing token", {
82
+ sent: csrfToken ? "[present]" : "[missing]",
83
+ stored: storedToken ? "[present]" : "[missing]",
84
+ });
85
+ logCsrfViolation(request, reason);
86
+ return NextResponse.json({ error: "Invalid CSRF token" }, { status: 403 });
87
+ }
88
+ if (!validateCsrfToken(csrfToken, storedToken)) {
89
+ console.error("CSRF check failed: Token mismatch");
90
+ logCsrfViolation(request, "CSRF token mismatch");
91
+ return NextResponse.json({ error: "Invalid CSRF token" }, { status: 403 });
92
+ }
93
+ // Call the original handler
94
+ const response = await handler(request);
95
+ // Clear the CSRF token after use (single-use tokens)
96
+ response.cookies.set("XSRF-TOKEN", "", { maxAge: 0, path: "/" });
97
+ return response;
98
+ }
99
+ catch (err) {
100
+ console.error("CSRF middleware error:", err.message);
101
+ return NextResponse.json({ error: "Server error" }, { status: 500 });
102
+ }
103
+ };
104
+ }
@@ -0,0 +1,23 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ /**
3
+ * Higher-order function that wraps API route handlers with rate limiting
4
+ * Tracks requests by IP address and returns 429 when limit is exceeded
5
+ *
6
+ * @param handler - The API route handler to wrap
7
+ * @returns A wrapped handler with rate limiting protection
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { withRateLimit } from '@/lib/withRateLimit';
12
+ * import { withCsrf } from '@/lib/withCsrf';
13
+ *
14
+ * async function sensitiveHandler(request: NextRequest) {
15
+ * // Your API logic here
16
+ * return NextResponse.json({ success: true });
17
+ * }
18
+ *
19
+ * // Apply both rate limiting and CSRF protection
20
+ * export const POST = withRateLimit(withCsrf(sensitiveHandler));
21
+ * ```
22
+ */
23
+ export declare function withRateLimit(handler: (request: NextRequest) => Promise<NextResponse>): (request: NextRequest) => Promise<NextResponse<unknown>>;
@@ -0,0 +1,168 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
3
+ import { ConvexHttpClient } from 'convex/browser';
4
+ import { makeFunctionReference } from 'convex/server';
5
+ /**
6
+ * The app's public security-log mutation. Named rather than imported from the
7
+ * app's generated api so this file can live in a package; the app registers
8
+ * `logSecurityViolation` in convex/security.ts with exactly these args.
9
+ */
10
+ const logSecurityViolation = makeFunctionReference('security:logSecurityViolation');
11
+ /**
12
+ * Rate limiter instance - 5 requests per minute per IP address
13
+ *
14
+ * SCOPE LIMITATION — READ BEFORE RELYING ON THIS FOR ABUSE PREVENTION.
15
+ *
16
+ * RateLimiterMemory keeps its counters in the memory of ONE process. Vercel runs
17
+ * this app across many function instances and recycles them freely, so:
18
+ *
19
+ * - a caller spread across N warm instances gets up to N x 5 requests/minute
20
+ * - counters reset whenever an instance is recycled
21
+ *
22
+ * That makes this a speed bump against naive scripted abuse, NOT a guarantee.
23
+ * It is honest defence in depth; it is not a quota.
24
+ *
25
+ * For a real limit you need shared durable state (Redis / Upstash) keyed the
26
+ * same way, or enforcement at a layer that already sees every request — Vercel's
27
+ * WAF rate limiting is the least-effort option and requires no code.
28
+ *
29
+ * For Convex mutations specifically this wrapper does nothing at all: Convex RPC
30
+ * goes browser -> Convex and never passes through a Next.js route handler. Limit
31
+ * those inside the mutation.
32
+ */
33
+ const rateLimiter = new RateLimiterMemory({
34
+ points: 5, // 5 requests
35
+ duration: 60, // per 60 seconds (1 minute)
36
+ });
37
+ /**
38
+ * Resolves the caller's IP from platform-trusted headers.
39
+ *
40
+ * Why not the leftmost X-Forwarded-For entry: XFF is an append-only list, and
41
+ * the LEFTMOST entry is whatever the client sent. A caller can simply set
42
+ * `X-Forwarded-For: <random>` on each request and receive a fresh bucket every
43
+ * time, defeating the limit entirely. The previous implementation did exactly
44
+ * this, so the limiter could be bypassed by anyone who knew to rotate a header.
45
+ *
46
+ * Order of preference:
47
+ * 1. `x-vercel-forwarded-for` — set by Vercel's edge, overwritten on every
48
+ * request, so a client cannot forge it.
49
+ * 2. The RIGHTMOST `x-forwarded-for` entry — appended by the closest trusted
50
+ * proxy, rather than the leftmost value the client supplied.
51
+ * 3. `x-real-ip` — set by the proxy, not the client, on common setups.
52
+ *
53
+ * If none are present we return null and the caller fails CLOSED (see below),
54
+ * because a single shared "unknown" bucket would let one abuser exhaust the
55
+ * limit for every anonymous visitor at once.
56
+ */
57
+ function clientIp(request) {
58
+ const vercel = request.headers.get('x-vercel-forwarded-for')?.trim();
59
+ if (vercel)
60
+ return vercel;
61
+ const xff = request.headers.get('x-forwarded-for');
62
+ if (xff) {
63
+ const parts = xff.split(',').map((p) => p.trim()).filter(Boolean);
64
+ if (parts.length > 0)
65
+ return parts[parts.length - 1];
66
+ }
67
+ const real = request.headers.get('x-real-ip')?.trim();
68
+ if (real)
69
+ return real;
70
+ return null;
71
+ }
72
+ // Lazy-initialized Convex client for security logging
73
+ let convexClient = null;
74
+ function getConvexClient() {
75
+ if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
76
+ return null;
77
+ }
78
+ if (!convexClient) {
79
+ convexClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL);
80
+ }
81
+ return convexClient;
82
+ }
83
+ /**
84
+ * Higher-order function that wraps API route handlers with rate limiting
85
+ * Tracks requests by IP address and returns 429 when limit is exceeded
86
+ *
87
+ * @param handler - The API route handler to wrap
88
+ * @returns A wrapped handler with rate limiting protection
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * import { withRateLimit } from '@/lib/withRateLimit';
93
+ * import { withCsrf } from '@/lib/withCsrf';
94
+ *
95
+ * async function sensitiveHandler(request: NextRequest) {
96
+ * // Your API logic here
97
+ * return NextResponse.json({ success: true });
98
+ * }
99
+ *
100
+ * // Apply both rate limiting and CSRF protection
101
+ * export const POST = withRateLimit(withCsrf(sensitiveHandler));
102
+ * ```
103
+ */
104
+ export function withRateLimit(handler) {
105
+ return async (request) => {
106
+ // Resolve once, outside the try, so the catch block reports the same key
107
+ // that was actually consumed rather than recomputing it.
108
+ const ip = clientIp(request);
109
+ if (!ip) {
110
+ // Fail closed. Previously an unresolvable IP collapsed to the literal
111
+ // string 'unknown', putting every such caller in ONE shared bucket — so a
112
+ // single abuser could exhaust the limit for all of them. Refusing is the
113
+ // safer failure, and on Vercel this branch should be unreachable.
114
+ console.warn('Rate limit: could not resolve a trusted client IP; refusing request');
115
+ return NextResponse.json({ error: 'Too many requests, please try again later' }, { status: 429 });
116
+ }
117
+ try {
118
+ // Consume a point for this IP address
119
+ await rateLimiter.consume(ip);
120
+ // Rate limit not exceeded, proceed with the original handler
121
+ return await handler(request);
122
+ }
123
+ catch (err) {
124
+ // Check if this is a rate limit error.
125
+ //
126
+ // instanceof, NOT `err.constructor.name === 'RateLimiterRes'`. The
127
+ // production server bundle is minified and class names are mangled, so
128
+ // the name check matched in `next dev` and silently failed in `next
129
+ // build` + `next start`: every over-limit request fell through to the
130
+ // rethrow below and surfaced as a 500, and the violation was never
131
+ // logged. Caught by the release smoke test, not by review.
132
+ if (err instanceof RateLimiterRes) {
133
+ const endpoint = request.nextUrl.pathname;
134
+ console.warn(`Rate limit exceeded for IP: ${ip} on ${endpoint}`);
135
+ // Log security violation to Convex. The mutation requires the shared
136
+ // SECURITY_LOG_KEY so that only this server, not anyone holding the
137
+ // public deployment URL, can write to the security log.
138
+ const client = getConvexClient();
139
+ const key = process.env.SECURITY_LOG_KEY;
140
+ if (client && !key) {
141
+ console.error('SECURITY_LOG_KEY is not set; rate limit violation not logged to Convex');
142
+ }
143
+ if (client && key) {
144
+ try {
145
+ await client.mutation(logSecurityViolation, {
146
+ key,
147
+ eventType: 'rate_limit_exceeded',
148
+ severity: 'medium',
149
+ metadata: {
150
+ endpoint,
151
+ ipAddress: ip,
152
+ errorMessage: 'Rate limit exceeded: 5 requests per minute',
153
+ },
154
+ });
155
+ }
156
+ catch (logErr) {
157
+ console.error('Failed to log rate limit violation:', logErr);
158
+ }
159
+ }
160
+ return NextResponse.json({ error: 'Too many requests, please try again later' }, { status: 429 });
161
+ }
162
+ // Re-throw any other errors
163
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
164
+ console.error('Rate limiting error:', errorMessage);
165
+ throw err;
166
+ }
167
+ };
168
+ }
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "secure-vibe-runtime",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "7.3.0",
4
+ "description": "The runtime security pieces of Secure Vibe Coding OS (secrets loading, CSRF, rate limiting, security log) as one package, so every app built from the template gets fixes by updating it.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Allen Harper",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/harperaa/secure-vibe-coding-OS.git",
11
+ "directory": "packages/secure-vibe-runtime"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./csrf": {
23
+ "types": "./dist/csrf.d.ts",
24
+ "default": "./dist/csrf.js"
25
+ },
26
+ "./secrets": {
27
+ "types": "./dist/secrets.d.ts",
28
+ "default": "./dist/secrets.js"
29
+ },
30
+ "./withCsrf": {
31
+ "types": "./dist/withCsrf.d.ts",
32
+ "default": "./dist/withCsrf.js"
33
+ },
34
+ "./withRateLimit": {
35
+ "types": "./dist/withRateLimit.d.ts",
36
+ "default": "./dist/withRateLimit.js"
37
+ },
38
+ "./convex": {
39
+ "types": "./dist/convex/index.d.ts",
40
+ "default": "./dist/convex/index.js"
41
+ }
42
+ },
43
+ "scripts": {
44
+ "build": "rm -rf dist && tsc -p tsconfig.json",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "peerDependencies": {
48
+ "convex": ">=1.31.0",
49
+ "next": ">=15.0.0"
50
+ },
51
+ "dependencies": {
52
+ "crypto-js": "^4.2.0",
53
+ "rate-limiter-flexible": "^8.1.0",
54
+ "uuid": "^14.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/crypto-js": "^4.2.2",
58
+ "@types/node": "^20",
59
+ "convex": "^1.31.3",
60
+ "next": "^16.4.2",
61
+ "typescript": "^5"
62
+ },
63
+ "engines": {
64
+ "node": ">=20"
65
+ },
66
+ "keywords": [
67
+ "security",
68
+ "nextjs",
69
+ "convex",
70
+ "csrf",
71
+ "rate-limit",
72
+ "secure-vibe-coding"
73
+ ]
74
+ }