specnav-middleware 0.2.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/dist/index.cjs +56 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @specnav/middleware
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Initial public release with trajectory prediction, 3-layer caching, DOM morphing, and all 19 audit bugs fixed.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Trajectory-based prefetch with cursor prediction
|
|
11
|
+
- 3-layer cache (Memory/ServiceWorker/Edge)
|
|
12
|
+
- Surgical DOM morphing with scroll/focus preservation
|
|
13
|
+
- Speculative rendering in detached containers
|
|
14
|
+
- Navigation graph learning for pattern-based prefetch
|
|
15
|
+
- Adaptive mode with battery/network awareness
|
|
16
|
+
- Next.js Link component (drop-in replacement)
|
|
17
|
+
- Edge middleware with rate limiting and CSRF protection
|
|
18
|
+
|
|
19
|
+
Bug fixes:
|
|
20
|
+
- Fixed trajectory prediction actually triggering prefetch
|
|
21
|
+
- Fixed navigation graph self-to-self transitions
|
|
22
|
+
- Fixed cache blocking Next.js **NEXT_DATA** scripts
|
|
23
|
+
- Fixed onNavigateEnd callback implementation
|
|
24
|
+
- Fixed middleware same-origin bypass
|
|
25
|
+
- Fixed null engines on first render
|
|
26
|
+
- Fixed LRU cache duplicate entries
|
|
27
|
+
- Fixed cache exclusion substring matching
|
|
28
|
+
- Fixed async adaptive initialization race
|
|
29
|
+
- Fixed duplicate Link unregistration
|
|
30
|
+
- Fixed morph refocus without containment check
|
|
31
|
+
- Fixed navigation timing polling
|
|
32
|
+
- Fixed cache hit rate tracking
|
|
33
|
+
- And 6 more fixes (see BUG_FIXES.md)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 specnav contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var server = require('next/server');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
var rateLimitMap = /* @__PURE__ */ new Map();
|
|
7
|
+
function checkRateLimit(ip, limit = 100, windowMs = 6e4) {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const record = rateLimitMap.get(ip);
|
|
10
|
+
if (!record || now > record.resetAt) {
|
|
11
|
+
rateLimitMap.set(ip, { count: 1, resetAt: now + windowMs });
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (record.count >= limit) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
record.count++;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
function specnavMiddleware(request) {
|
|
21
|
+
const isSpecnav = request.headers.get("x-specnav") === "1";
|
|
22
|
+
if (!isSpecnav) return null;
|
|
23
|
+
const ip = request.headers.get("x-forwarded-for")?.split(",")[0] || request.headers.get("x-real-ip") || "unknown";
|
|
24
|
+
if (!checkRateLimit(ip)) {
|
|
25
|
+
return new server.NextResponse("Too Many Requests", { status: 429 });
|
|
26
|
+
}
|
|
27
|
+
const origin = request.headers.get("origin");
|
|
28
|
+
const referer = request.headers.get("referer");
|
|
29
|
+
if (!origin && !referer) {
|
|
30
|
+
return new server.NextResponse("Forbidden", { status: 403 });
|
|
31
|
+
}
|
|
32
|
+
const requestOrigin = origin || new URL(referer).origin;
|
|
33
|
+
const serverOrigin = new URL(request.url).origin;
|
|
34
|
+
if (requestOrigin !== serverOrigin) {
|
|
35
|
+
return new server.NextResponse("Forbidden", { status: 403 });
|
|
36
|
+
}
|
|
37
|
+
const headers = new Headers(request.headers);
|
|
38
|
+
headers.set("x-specnav-partial", "1");
|
|
39
|
+
return server.NextResponse.next({
|
|
40
|
+
request: {
|
|
41
|
+
headers
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function isSpecnavRequest(request) {
|
|
46
|
+
return request.headers.get("x-specnav") === "1";
|
|
47
|
+
}
|
|
48
|
+
function isPartialRequest(request) {
|
|
49
|
+
return request.headers.get("x-specnav-partial") === "1";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
exports.isPartialRequest = isPartialRequest;
|
|
53
|
+
exports.isSpecnavRequest = isSpecnavRequest;
|
|
54
|
+
exports.specnavMiddleware = specnavMiddleware;
|
|
55
|
+
//# sourceMappingURL=index.cjs.map
|
|
56
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["NextResponse"],"mappings":";;;;;AAOA,IAAM,YAAA,uBAAmB,GAAA,EAAgD;AAEzE,SAAS,cAAA,CAAe,EAAA,EAAY,KAAA,GAAQ,GAAA,EAAK,WAAW,GAAA,EAAgB;AAC1E,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,GAAA,CAAI,EAAE,CAAA;AAElC,EAAA,IAAI,CAAC,MAAA,IAAU,GAAA,GAAM,MAAA,CAAO,OAAA,EAAS;AACnC,IAAA,YAAA,CAAa,GAAA,CAAI,IAAI,EAAE,KAAA,EAAO,GAAG,OAAA,EAAS,GAAA,GAAM,UAAU,CAAA;AAC1D,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,MAAA,CAAO,SAAS,KAAA,EAAO;AACzB,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,MAAA,CAAO,KAAA,EAAA;AACP,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,kBAAkB,OAAA,EAA2C;AAC3E,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA,KAAM,GAAA;AAEvD,EAAA,IAAI,CAAC,WAAW,OAAO,IAAA;AAGvB,EAAA,MAAM,EAAA,GAAK,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,iBAAiB,CAAA,EAAG,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA,IACpD,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA,IAC/B,SAAA;AACX,EAAA,IAAI,CAAC,cAAA,CAAe,EAAE,CAAA,EAAG;AACvB,IAAA,OAAO,IAAIA,mBAAA,CAAa,mBAAA,EAAqB,EAAE,MAAA,EAAQ,KAAK,CAAA;AAAA,EAC9D;AAGA,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AAC3C,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,SAAS,CAAA;AAE7C,EAAA,IAAI,CAAC,MAAA,IAAU,CAAC,OAAA,EAAS;AACvB,IAAA,OAAO,IAAIA,mBAAA,CAAa,WAAA,EAAa,EAAE,MAAA,EAAQ,KAAK,CAAA;AAAA,EACtD;AAEA,EAAA,MAAM,aAAA,GAAgB,MAAA,IAAU,IAAI,GAAA,CAAI,OAAQ,CAAA,CAAE,MAAA;AAClD,EAAA,MAAM,YAAA,GAAe,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA,CAAE,MAAA;AAE1C,EAAA,IAAI,kBAAkB,YAAA,EAAc;AAClC,IAAA,OAAO,IAAIA,mBAAA,CAAa,WAAA,EAAa,EAAE,MAAA,EAAQ,KAAK,CAAA;AAAA,EACtD;AAGA,EAAA,MAAM,OAAA,GAAU,IAAI,OAAA,CAAQ,OAAA,CAAQ,OAAO,CAAA;AAC3C,EAAA,OAAA,CAAQ,GAAA,CAAI,qBAAqB,GAAG,CAAA;AAEpC,EAAA,OAAOA,oBAAa,IAAA,CAAK;AAAA,IACvB,OAAA,EAAS;AAAA,MACP;AAAA;AACF,GACD,CAAA;AACH;AAEO,SAAS,iBAAiB,OAAA,EAA+B;AAC9D,EAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA,KAAM,GAAA;AAC9C;AAEO,SAAS,iBAAiB,OAAA,EAA+B;AAC9D,EAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,mBAAmB,CAAA,KAAM,GAAA;AACtD","file":"index.cjs","sourcesContent":["import { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\n\n// Simple in-memory rate limiter\n// ⚠️ WARNING: This rate limiter is NOT effective on serverless platforms (Vercel Edge, AWS Lambda, etc.)\n// where each cold start creates a fresh module scope, resetting the Map. For production serverless use,\n// replace with a persistent store like Redis, Upstash, Vercel KV, or a distributed rate limiting service.\nconst rateLimitMap = new Map<string, { count: number; resetAt: number }>();\n\nfunction checkRateLimit(ip: string, limit = 100, windowMs = 60000): boolean {\n const now = Date.now();\n const record = rateLimitMap.get(ip);\n\n if (!record || now > record.resetAt) {\n rateLimitMap.set(ip, { count: 1, resetAt: now + windowMs });\n return true;\n }\n\n if (record.count >= limit) {\n return false;\n }\n\n record.count++;\n return true;\n}\n\nexport function specnavMiddleware(request: NextRequest): NextResponse | null {\n const isSpecnav = request.headers.get(\"x-specnav\") === \"1\";\n\n if (!isSpecnav) return null;\n\n // Rate limiting\n const ip = request.headers.get(\"x-forwarded-for\")?.split(\",\")[0] || \n request.headers.get(\"x-real-ip\") || \n \"unknown\";\n if (!checkRateLimit(ip)) {\n return new NextResponse(\"Too Many Requests\", { status: 429 });\n }\n\n // Security: Verify same-origin (deny if headers missing)\n const origin = request.headers.get(\"origin\");\n const referer = request.headers.get(\"referer\");\n \n if (!origin && !referer) {\n return new NextResponse(\"Forbidden\", { status: 403 });\n }\n \n const requestOrigin = origin || new URL(referer!).origin;\n const serverOrigin = new URL(request.url).origin;\n \n if (requestOrigin !== serverOrigin) {\n return new NextResponse(\"Forbidden\", { status: 403 });\n }\n\n // Clone request and add internal header for route handlers\n const headers = new Headers(request.headers);\n headers.set(\"x-specnav-partial\", \"1\");\n\n return NextResponse.next({\n request: {\n headers,\n },\n });\n}\n\nexport function isSpecnavRequest(request: NextRequest): boolean {\n return request.headers.get(\"x-specnav\") === \"1\";\n}\n\nexport function isPartialRequest(request: NextRequest): boolean {\n return request.headers.get(\"x-specnav-partial\") === \"1\";\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
declare function specnavMiddleware(request: NextRequest): NextResponse | null;
|
|
4
|
+
declare function isSpecnavRequest(request: NextRequest): boolean;
|
|
5
|
+
declare function isPartialRequest(request: NextRequest): boolean;
|
|
6
|
+
|
|
7
|
+
export { isPartialRequest, isSpecnavRequest, specnavMiddleware };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
declare function specnavMiddleware(request: NextRequest): NextResponse | null;
|
|
4
|
+
declare function isSpecnavRequest(request: NextRequest): boolean;
|
|
5
|
+
declare function isPartialRequest(request: NextRequest): boolean;
|
|
6
|
+
|
|
7
|
+
export { isPartialRequest, isSpecnavRequest, specnavMiddleware };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var rateLimitMap = /* @__PURE__ */ new Map();
|
|
5
|
+
function checkRateLimit(ip, limit = 100, windowMs = 6e4) {
|
|
6
|
+
const now = Date.now();
|
|
7
|
+
const record = rateLimitMap.get(ip);
|
|
8
|
+
if (!record || now > record.resetAt) {
|
|
9
|
+
rateLimitMap.set(ip, { count: 1, resetAt: now + windowMs });
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (record.count >= limit) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
record.count++;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
function specnavMiddleware(request) {
|
|
19
|
+
const isSpecnav = request.headers.get("x-specnav") === "1";
|
|
20
|
+
if (!isSpecnav) return null;
|
|
21
|
+
const ip = request.headers.get("x-forwarded-for")?.split(",")[0] || request.headers.get("x-real-ip") || "unknown";
|
|
22
|
+
if (!checkRateLimit(ip)) {
|
|
23
|
+
return new NextResponse("Too Many Requests", { status: 429 });
|
|
24
|
+
}
|
|
25
|
+
const origin = request.headers.get("origin");
|
|
26
|
+
const referer = request.headers.get("referer");
|
|
27
|
+
if (!origin && !referer) {
|
|
28
|
+
return new NextResponse("Forbidden", { status: 403 });
|
|
29
|
+
}
|
|
30
|
+
const requestOrigin = origin || new URL(referer).origin;
|
|
31
|
+
const serverOrigin = new URL(request.url).origin;
|
|
32
|
+
if (requestOrigin !== serverOrigin) {
|
|
33
|
+
return new NextResponse("Forbidden", { status: 403 });
|
|
34
|
+
}
|
|
35
|
+
const headers = new Headers(request.headers);
|
|
36
|
+
headers.set("x-specnav-partial", "1");
|
|
37
|
+
return NextResponse.next({
|
|
38
|
+
request: {
|
|
39
|
+
headers
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function isSpecnavRequest(request) {
|
|
44
|
+
return request.headers.get("x-specnav") === "1";
|
|
45
|
+
}
|
|
46
|
+
function isPartialRequest(request) {
|
|
47
|
+
return request.headers.get("x-specnav-partial") === "1";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { isPartialRequest, isSpecnavRequest, specnavMiddleware };
|
|
51
|
+
//# sourceMappingURL=index.js.map
|
|
52
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAOA,IAAM,YAAA,uBAAmB,GAAA,EAAgD;AAEzE,SAAS,cAAA,CAAe,EAAA,EAAY,KAAA,GAAQ,GAAA,EAAK,WAAW,GAAA,EAAgB;AAC1E,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,GAAA,CAAI,EAAE,CAAA;AAElC,EAAA,IAAI,CAAC,MAAA,IAAU,GAAA,GAAM,MAAA,CAAO,OAAA,EAAS;AACnC,IAAA,YAAA,CAAa,GAAA,CAAI,IAAI,EAAE,KAAA,EAAO,GAAG,OAAA,EAAS,GAAA,GAAM,UAAU,CAAA;AAC1D,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,MAAA,CAAO,SAAS,KAAA,EAAO;AACzB,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,MAAA,CAAO,KAAA,EAAA;AACP,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,kBAAkB,OAAA,EAA2C;AAC3E,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA,KAAM,GAAA;AAEvD,EAAA,IAAI,CAAC,WAAW,OAAO,IAAA;AAGvB,EAAA,MAAM,EAAA,GAAK,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,iBAAiB,CAAA,EAAG,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA,IACpD,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA,IAC/B,SAAA;AACX,EAAA,IAAI,CAAC,cAAA,CAAe,EAAE,CAAA,EAAG;AACvB,IAAA,OAAO,IAAI,YAAA,CAAa,mBAAA,EAAqB,EAAE,MAAA,EAAQ,KAAK,CAAA;AAAA,EAC9D;AAGA,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AAC3C,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,SAAS,CAAA;AAE7C,EAAA,IAAI,CAAC,MAAA,IAAU,CAAC,OAAA,EAAS;AACvB,IAAA,OAAO,IAAI,YAAA,CAAa,WAAA,EAAa,EAAE,MAAA,EAAQ,KAAK,CAAA;AAAA,EACtD;AAEA,EAAA,MAAM,aAAA,GAAgB,MAAA,IAAU,IAAI,GAAA,CAAI,OAAQ,CAAA,CAAE,MAAA;AAClD,EAAA,MAAM,YAAA,GAAe,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA,CAAE,MAAA;AAE1C,EAAA,IAAI,kBAAkB,YAAA,EAAc;AAClC,IAAA,OAAO,IAAI,YAAA,CAAa,WAAA,EAAa,EAAE,MAAA,EAAQ,KAAK,CAAA;AAAA,EACtD;AAGA,EAAA,MAAM,OAAA,GAAU,IAAI,OAAA,CAAQ,OAAA,CAAQ,OAAO,CAAA;AAC3C,EAAA,OAAA,CAAQ,GAAA,CAAI,qBAAqB,GAAG,CAAA;AAEpC,EAAA,OAAO,aAAa,IAAA,CAAK;AAAA,IACvB,OAAA,EAAS;AAAA,MACP;AAAA;AACF,GACD,CAAA;AACH;AAEO,SAAS,iBAAiB,OAAA,EAA+B;AAC9D,EAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA,KAAM,GAAA;AAC9C;AAEO,SAAS,iBAAiB,OAAA,EAA+B;AAC9D,EAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,mBAAmB,CAAA,KAAM,GAAA;AACtD","file":"index.js","sourcesContent":["import { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\n\n// Simple in-memory rate limiter\n// ⚠️ WARNING: This rate limiter is NOT effective on serverless platforms (Vercel Edge, AWS Lambda, etc.)\n// where each cold start creates a fresh module scope, resetting the Map. For production serverless use,\n// replace with a persistent store like Redis, Upstash, Vercel KV, or a distributed rate limiting service.\nconst rateLimitMap = new Map<string, { count: number; resetAt: number }>();\n\nfunction checkRateLimit(ip: string, limit = 100, windowMs = 60000): boolean {\n const now = Date.now();\n const record = rateLimitMap.get(ip);\n\n if (!record || now > record.resetAt) {\n rateLimitMap.set(ip, { count: 1, resetAt: now + windowMs });\n return true;\n }\n\n if (record.count >= limit) {\n return false;\n }\n\n record.count++;\n return true;\n}\n\nexport function specnavMiddleware(request: NextRequest): NextResponse | null {\n const isSpecnav = request.headers.get(\"x-specnav\") === \"1\";\n\n if (!isSpecnav) return null;\n\n // Rate limiting\n const ip = request.headers.get(\"x-forwarded-for\")?.split(\",\")[0] || \n request.headers.get(\"x-real-ip\") || \n \"unknown\";\n if (!checkRateLimit(ip)) {\n return new NextResponse(\"Too Many Requests\", { status: 429 });\n }\n\n // Security: Verify same-origin (deny if headers missing)\n const origin = request.headers.get(\"origin\");\n const referer = request.headers.get(\"referer\");\n \n if (!origin && !referer) {\n return new NextResponse(\"Forbidden\", { status: 403 });\n }\n \n const requestOrigin = origin || new URL(referer!).origin;\n const serverOrigin = new URL(request.url).origin;\n \n if (requestOrigin !== serverOrigin) {\n return new NextResponse(\"Forbidden\", { status: 403 });\n }\n\n // Clone request and add internal header for route handlers\n const headers = new Headers(request.headers);\n headers.set(\"x-specnav-partial\", \"1\");\n\n return NextResponse.next({\n request: {\n headers,\n },\n });\n}\n\nexport function isSpecnavRequest(request: NextRequest): boolean {\n return request.headers.get(\"x-specnav\") === \"1\";\n}\n\nexport function isPartialRequest(request: NextRequest): boolean {\n return request.headers.get(\"x-specnav-partial\") === \"1\";\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "specnav-middleware",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Next.js Edge middleware for specnav with rate limiting, CSRF protection, and partial response rendering",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Abraham",
|
|
7
|
+
"homepage": "https://github.com/Robini908/specnav#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/Robini908/specnav.git",
|
|
11
|
+
"directory": "packages/middleware"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Robini908/specnav/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"nextjs",
|
|
18
|
+
"next.js",
|
|
19
|
+
"middleware",
|
|
20
|
+
"edge",
|
|
21
|
+
"partial-response",
|
|
22
|
+
"rate-limiting",
|
|
23
|
+
"csrf"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"module": "./dist/index.mjs",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.mjs",
|
|
34
|
+
"require": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"CHANGELOG.md"
|
|
42
|
+
],
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"next": ">=14.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.0.0",
|
|
48
|
+
"next": "^15.0.0",
|
|
49
|
+
"tsup": "^8.0.2",
|
|
50
|
+
"typescript": "^5.8.0",
|
|
51
|
+
"vitest": "^2.0.0"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public",
|
|
58
|
+
"registry": "https://registry.npmjs.org"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsup",
|
|
62
|
+
"dev": "tsup --watch",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest",
|
|
65
|
+
"typecheck": "tsc --noEmit"
|
|
66
|
+
}
|
|
67
|
+
}
|