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 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"]}
@@ -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 };
@@ -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
+ }