@spring-systems/server 0.8.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 +62 -0
- package/LICENSE +8 -0
- package/README.md +94 -0
- package/dist/api-route-handler.d.ts +49 -0
- package/dist/api-route-handler.js +19 -0
- package/dist/api-route-handler.js.map +1 -0
- package/dist/chunk-7IUSTA5W.js +113 -0
- package/dist/chunk-7IUSTA5W.js.map +1 -0
- package/dist/chunk-CLZU34DG.js +465 -0
- package/dist/chunk-CLZU34DG.js.map +1 -0
- package/dist/chunk-CP33WQ5Q.js +47 -0
- package/dist/chunk-CP33WQ5Q.js.map +1 -0
- package/dist/chunk-FEB3UZEG.js +407 -0
- package/dist/chunk-FEB3UZEG.js.map +1 -0
- package/dist/chunk-KA7RJCWA.js +24 -0
- package/dist/chunk-KA7RJCWA.js.map +1 -0
- package/dist/chunk-OYTV4D7E.js +159 -0
- package/dist/chunk-OYTV4D7E.js.map +1 -0
- package/dist/chunk-YV6DZVPI.js +43 -0
- package/dist/chunk-YV6DZVPI.js.map +1 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +14 -0
- package/dist/client.js.map +1 -0
- package/dist/handlers/index.d.ts +81 -0
- package/dist/handlers/index.js +48 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/next-adapters.d.ts +25 -0
- package/dist/next-adapters.js +14 -0
- package/dist/next-adapters.js.map +1 -0
- package/dist/proxy-middleware.d.ts +8 -0
- package/dist/proxy-middleware.js +10 -0
- package/dist/proxy-middleware.js.map +1 -0
- package/dist/rate-limiter.d.ts +67 -0
- package/dist/rate-limiter.js +15 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/runtime-env.d.ts +15 -0
- package/dist/runtime-env.js +9 -0
- package/dist/runtime-env.js.map +1 -0
- package/dist/security-headers.d.ts +8 -0
- package/dist/security-headers.js +11 -0
- package/dist/security-headers.js.map +1 -0
- package/package.json +114 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/proxy-middleware.ts"],"sourcesContent":["import { getFrameworkConfig } from \"@spring-systems/core/config\";\nimport { type NextRequest, NextResponse } from \"next/server.js\";\n\nimport { BASE_SECURITY_HEADER_VALUES } from \"./security-headers\";\n\nfunction toOriginSource(value: string): string[] {\n const raw = value.trim();\n if (!raw) return [];\n\n try {\n const parsed = new URL(raw);\n const out = [parsed.origin];\n\n if (parsed.protocol === \"https:\") {\n out.push(`wss://${parsed.host}`);\n } else if (parsed.protocol === \"http:\") {\n out.push(`ws://${parsed.host}`);\n }\n\n return out;\n } catch {\n return [];\n }\n}\n\nconst BLOCKED_CSP_SCHEMES = new Set([\"javascript:\", \"data:\", \"blob:\", \"vbscript:\", \"filesystem:\"]);\n\nfunction parseCspSource(token: string): string | null {\n const value = token.trim();\n if (!value) return null;\n\n // Prevent header/CSP injection via env values.\n if (/[\\s;,\\r\\n]/.test(value)) return null;\n\n // Allow safe scheme sources (e.g. https:, wss:). Block dangerous schemes\n // that enable XSS or data exfiltration when used in CSP directives.\n if (/^[a-zA-Z][a-zA-Z0-9+.-]*:$/.test(value)) {\n const lower = value.toLowerCase();\n if (BLOCKED_CSP_SCHEMES.has(lower)) return null;\n return lower;\n }\n\n try {\n const parsed = new URL(value);\n if (![\"http:\", \"https:\", \"ws:\", \"wss:\"].includes(parsed.protocol)) return null;\n if (parsed.username || parsed.password) return null;\n if (parsed.pathname !== \"/\" || parsed.search || parsed.hash) return null;\n return `${parsed.protocol}//${parsed.host}`;\n } catch {\n return null;\n }\n}\n\nfunction parseReportUri(value: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n // Block header injection characters and double-quotes (used in Reporting-Endpoints structured header).\n if (/[\\s;,\\r\\n\"]/.test(trimmed)) return null;\n try {\n const parsed = new URL(trimmed);\n if (![\"http:\", \"https:\"].includes(parsed.protocol)) return null;\n if (parsed.username || parsed.password) return null;\n return parsed.toString();\n } catch {\n return null;\n }\n}\n\nfunction parseCspList(value: string): string[] {\n const parsed = value\n .split(\",\")\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n return [...new Set(parsed)];\n}\n\nexport function proxy(req: NextRequest) {\n const url = req.nextUrl.clone();\n\n const TARGET_ENV = process.env.TARGET_ENV || \"test\";\n const isProdRuntime = TARGET_ENV === \"prod\" || process.env.NODE_ENV === \"production\";\n const isTargetProd = TARGET_ENV === \"prod\";\n\n const isLocalHost = url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\";\n\n const nonce = crypto.randomUUID().replace(/-/g, \"\");\n const allowUnsafeStyleAttr = process.env.CSP_ALLOW_UNSAFE_STYLE_ATTR === \"true\";\n const denyStyleAttr = isTargetProd && !allowUnsafeStyleAttr;\n const allowUnsafeInlineStyle = !isTargetProd;\n\n const connectHostsEnv = parseCspList(process.env.CSP_CONNECT_HOSTS || \"\");\n const apiConnectSources = toOriginSource(process.env.API_URL || \"\");\n const imgHostsEnv = parseCspList(process.env.CSP_IMG_HOSTS || \"\");\n const scriptHostsEnv = parseCspList(process.env.CSP_SCRIPT_HOSTS || \"\");\n const frameHostsEnv = parseCspList(process.env.CSP_FRAME_HOSTS || \"\");\n\n const cspConfig = getFrameworkConfig().csp;\n\n // Validate framework config CSP sources through parseCspSource to prevent injection\n const validatedConnectSources = cspConfig.thirdPartyConnectSources.map((s) => parseCspSource(s)).filter((s): s is string => !!s);\n const validatedImgSources = cspConfig.thirdPartyImageSources.map((s) => parseCspSource(s)).filter((s): s is string => !!s);\n const validatedScriptSources = cspConfig.thirdPartyScriptSources.map((s) => parseCspSource(s)).filter((s): s is string => !!s);\n const validatedFrameSources = cspConfig.thirdPartyFrameSources.map((s) => parseCspSource(s)).filter((s): s is string => !!s);\n\n const connectSrc = [\n \"'self'\",\n ...validatedConnectSources,\n ...apiConnectSources,\n ...connectHostsEnv,\n ...(!isProdRuntime ? [\"http://localhost:*\", \"http://127.0.0.1:*\", \"ws://localhost:*\", \"ws://127.0.0.1:*\"] : []),\n ].join(\" \");\n\n const imgSrc = [\"'self'\", \"data:\", \"blob:\", ...validatedImgSources, ...imgHostsEnv].join(\" \");\n\n const scriptSrcHosts = [...validatedScriptSources, ...scriptHostsEnv].join(\" \");\n const frameSrcHosts = [...validatedFrameSources, ...frameHostsEnv].join(\" \");\n\n const reportUri = parseReportUri(process.env.CSP_REPORT_URI || \"\");\n\n const cspDirectives = [\n \"default-src 'self'\",\n \"base-uri 'self'\",\n \"object-src 'none'\",\n `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,\n `script-src-elem 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,\n \"script-src-attr 'none'\",\n allowUnsafeInlineStyle ? \"style-src 'self' 'unsafe-inline'\" : `style-src 'self' 'nonce-${nonce}'`,\n denyStyleAttr ? \"style-src-attr 'none'\" : \"style-src-attr 'unsafe-inline'\",\n `img-src ${imgSrc}`,\n \"media-src 'self' blob: data:\",\n \"manifest-src 'self'\",\n `connect-src ${connectSrc}`,\n `frame-src ${frameSrcHosts || \"'none'\"}`,\n \"frame-ancestors 'none'\",\n \"form-action 'self'\",\n ...(isTargetProd ? [\"upgrade-insecure-requests\"] : []),\n ...(reportUri ? [`report-uri ${reportUri}`, \"report-to csp-violations\"] : []),\n ];\n const csp = cspDirectives.join(\"; \");\n\n const blockedPrefixes = getFrameworkConfig().proxy.blockedPathPrefixesInProd ?? [];\n if (isProdRuntime) {\n for (const prefix of blockedPrefixes) {\n if (url.pathname.startsWith(prefix)) {\n const res = new NextResponse(\"Not Found\", { status: 404 });\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n }\n }\n }\n\n if (url.pathname === \"/\") {\n const dest = new URL(url.toString());\n if (!isLocalHost) dest.protocol = \"https:\";\n dest.pathname = getFrameworkConfig().app.defaultRoute;\n dest.search = \"\";\n\n const res = makeRedirect(dest, 308);\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n }\n\n const requestHeaders = new Headers(req.headers);\n requestHeaders.set(\"x-nonce\", nonce);\n\n const res = NextResponse.next({ request: { headers: requestHeaders } });\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n}\n\nfunction setSecurity(\n res: NextResponse,\n opts: { isLocalHost: boolean; isTargetProd: boolean; csp?: string; nonce?: string; reportUri?: string | null }\n) {\n const { isLocalHost, csp, nonce, reportUri } = opts;\n if (csp) res.headers.set(\"Content-Security-Policy\", csp);\n for (const [key, value] of Object.entries(BASE_SECURITY_HEADER_VALUES)) {\n res.headers.set(key, value);\n }\n if (opts.isTargetProd && !isLocalHost)\n res.headers.set(\"Strict-Transport-Security\", \"max-age=63072000; includeSubDomains; preload\");\n if (reportUri) res.headers.set(\"Reporting-Endpoints\", `csp-violations=\"${reportUri}\"`);\n if (nonce) res.headers.set(\"x-nonce\", nonce);\n return res;\n}\n\nfunction makeRedirect(to: URL | string, status = 308) {\n const res = new NextResponse(null, { status });\n res.headers.set(\"Location\", typeof to === \"string\" ? to : to.toString());\n return res;\n}\n\nexport const proxyConfig = {\n matcher: [\"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)\"],\n};\n"],"mappings":"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","names":["res"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/runtime-env-route.ts
|
|
2
|
+
import { getRuntimeEnvKeys } from "@spring-systems/core/config";
|
|
3
|
+
import { NextResponse } from "next/server.js";
|
|
4
|
+
async function GET() {
|
|
5
|
+
const isProd = (process.env.NODE_ENV || "") === "production" || (process.env.TARGET_ENV || "") === "prod";
|
|
6
|
+
const allowInProd = (process.env.RUNTIME_ENV_PUBLIC_IN_PROD || "").toLowerCase() === "true";
|
|
7
|
+
if (isProd && !allowInProd) {
|
|
8
|
+
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
9
|
+
}
|
|
10
|
+
const filtered = Object.fromEntries(
|
|
11
|
+
getRuntimeEnvKeys().map((key) => [key, process.env[key]]).filter(([, value]) => typeof value === "string")
|
|
12
|
+
);
|
|
13
|
+
return NextResponse.json(filtered, {
|
|
14
|
+
status: 200,
|
|
15
|
+
headers: {
|
|
16
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
|
|
17
|
+
Pragma: "no-cache",
|
|
18
|
+
Expires: "0"
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/RuntimeEnvScript.tsx
|
|
24
|
+
import { getRuntimeEnvKeys as getRuntimeEnvKeys2 } from "@spring-systems/core/config";
|
|
25
|
+
import { jsx } from "react/jsx-runtime";
|
|
26
|
+
function serializeForInlineScript(value) {
|
|
27
|
+
return JSON.stringify(value).replace(/</g, "\\u003C").replace(/>/g, "\\u003E").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
28
|
+
}
|
|
29
|
+
function RuntimeEnvScript({ nonce }) {
|
|
30
|
+
const runtimeEnv = {};
|
|
31
|
+
for (const k of getRuntimeEnvKeys2()) {
|
|
32
|
+
const v = process.env[k];
|
|
33
|
+
if (typeof v === "string") runtimeEnv[k] = v;
|
|
34
|
+
}
|
|
35
|
+
const inline = `window.__RUNTIME_ENV__ = ${serializeForInlineScript(runtimeEnv)};`;
|
|
36
|
+
return /* @__PURE__ */ jsx("script", { id: "runtime-env", nonce, suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: inline } });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
GET,
|
|
41
|
+
RuntimeEnvScript
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=chunk-YV6DZVPI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runtime-env-route.ts","../src/RuntimeEnvScript.tsx"],"sourcesContent":["import { getRuntimeEnvKeys } from \"@spring-systems/core/config\";\nimport { NextResponse } from \"next/server.js\";\n\nexport async function GET() {\n const isProd = (process.env.NODE_ENV || \"\") === \"production\" || (process.env.TARGET_ENV || \"\") === \"prod\";\n const allowInProd = (process.env.RUNTIME_ENV_PUBLIC_IN_PROD || \"\").toLowerCase() === \"true\";\n if (isProd && !allowInProd) {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n }\n\n const filtered = Object.fromEntries(\n getRuntimeEnvKeys()\n .map((key) => [key, process.env[key]] as const)\n .filter(([, value]) => typeof value === \"string\")\n );\n\n return NextResponse.json(filtered, {\n status: 200,\n headers: {\n \"Cache-Control\": \"no-store, no-cache, must-revalidate, proxy-revalidate\",\n Pragma: \"no-cache\",\n Expires: \"0\",\n },\n });\n}\n","import { getRuntimeEnvKeys } from \"@spring-systems/core/config\";\n\nfunction serializeForInlineScript(value: unknown): string {\n return JSON.stringify(value)\n .replace(/</g, \"\\\\u003C\")\n .replace(/>/g, \"\\\\u003E\")\n .replace(/&/g, \"\\\\u0026\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\nexport interface RuntimeEnvScriptProps {\n nonce?: string;\n}\n\nexport function RuntimeEnvScript({ nonce }: RuntimeEnvScriptProps) {\n const runtimeEnv: Record<string, string> = {};\n for (const k of getRuntimeEnvKeys()) {\n const v = process.env[k];\n if (typeof v === \"string\") runtimeEnv[k] = v;\n }\n\n const inline = `window.__RUNTIME_ENV__ = ${serializeForInlineScript(runtimeEnv)};`;\n\n return (\n <script id=\"runtime-env\" nonce={nonce} suppressHydrationWarning dangerouslySetInnerHTML={{ __html: inline }} />\n );\n}\n"],"mappings":";AAAA,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAE7B,eAAsB,MAAM;AACxB,QAAM,UAAU,QAAQ,IAAI,YAAY,QAAQ,iBAAiB,QAAQ,IAAI,cAAc,QAAQ;AACnG,QAAM,eAAe,QAAQ,IAAI,8BAA8B,IAAI,YAAY,MAAM;AACrF,MAAI,UAAU,CAAC,aAAa;AACxB,WAAO,aAAa,KAAK,EAAE,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpE;AAEA,QAAM,WAAW,OAAO;AAAA,IACpB,kBAAkB,EACb,IAAI,CAAC,QAAQ,CAAC,KAAK,QAAQ,IAAI,GAAG,CAAC,CAAU,EAC7C,OAAO,CAAC,CAAC,EAAE,KAAK,MAAM,OAAO,UAAU,QAAQ;AAAA,EACxD;AAEA,SAAO,aAAa,KAAK,UAAU;AAAA,IAC/B,QAAQ;AAAA,IACR,SAAS;AAAA,MACL,iBAAiB;AAAA,MACjB,QAAQ;AAAA,MACR,SAAS;AAAA,IACb;AAAA,EACJ,CAAC;AACL;;;ACxBA,SAAS,qBAAAA,0BAAyB;AAyB1B;AAvBR,SAAS,yBAAyB,OAAwB;AACtD,SAAO,KAAK,UAAU,KAAK,EACtB,QAAQ,MAAM,SAAS,EACvB,QAAQ,MAAM,SAAS,EACvB,QAAQ,MAAM,SAAS,EACvB,QAAQ,WAAW,SAAS,EAC5B,QAAQ,WAAW,SAAS;AACrC;AAMO,SAAS,iBAAiB,EAAE,MAAM,GAA0B;AAC/D,QAAM,aAAqC,CAAC;AAC5C,aAAW,KAAKA,mBAAkB,GAAG;AACjC,UAAM,IAAI,QAAQ,IAAI,CAAC;AACvB,QAAI,OAAO,MAAM,SAAU,YAAW,CAAC,IAAI;AAAA,EAC/C;AAEA,QAAM,SAAS,4BAA4B,yBAAyB,UAAU,CAAC;AAE/E,SACI,oBAAC,YAAO,IAAG,eAAc,OAAc,0BAAwB,MAAC,yBAAyB,EAAE,QAAQ,OAAO,GAAG;AAErH;","names":["getRuntimeEnvKeys"]}
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
createClientOnlyNextUIAdapter,
|
|
4
|
+
createNextRouteAdapter
|
|
5
|
+
} from "./chunk-CP33WQ5Q.js";
|
|
6
|
+
|
|
7
|
+
// src/client.ts
|
|
8
|
+
var SPRING_SERVER_VERSION = true ? "0.8.0" : "0.1.0";
|
|
9
|
+
export {
|
|
10
|
+
SPRING_SERVER_VERSION,
|
|
11
|
+
createClientOnlyNextUIAdapter,
|
|
12
|
+
createNextRouteAdapter
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["\"use client\";\n\ndeclare const __VERSION__: string;\nexport const SPRING_SERVER_VERSION: string = typeof __VERSION__ !== \"undefined\" ? __VERSION__ : \"0.1.0\";\n\nexport { createClientOnlyNextUIAdapter, createNextRouteAdapter, type NextUIAdapterOptions } from \"./next-adapters\";\n"],"mappings":";;;;;;;AAGO,IAAM,wBAAgC,OAAqC,UAAc;","names":[]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { NextResponse, NextRequest } from 'next/server';
|
|
2
|
+
export { RateLimitEntry } from '../rate-limiter.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session/cookie management for the API proxy.
|
|
6
|
+
*
|
|
7
|
+
* Handles session token extraction from cookies, secure cookie setting/clearing,
|
|
8
|
+
* and session expiry detection from API responses.
|
|
9
|
+
*
|
|
10
|
+
* @module handlers/auth-session
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Check if a request targets localhost. */
|
|
14
|
+
declare function isLocalHostRequest(req: NextRequest): boolean;
|
|
15
|
+
/** Whether to use Secure flag on cookies (HTTPS or TARGET_ENV=prod, excluding localhost). */
|
|
16
|
+
declare function useSecureCookies(req: NextRequest): boolean;
|
|
17
|
+
/** Resolve the correct session cookie name based on runtime context. */
|
|
18
|
+
declare function resolveSessionCookieName(req: NextRequest): string;
|
|
19
|
+
/** Set the session cookie with the given token. Clears the stale variant. */
|
|
20
|
+
declare function setSessionCookie(res: NextResponse, req: NextRequest, token: string): void;
|
|
21
|
+
/** Clear all session cookies (both regular and secure variants). */
|
|
22
|
+
declare function clearSessionCookie(res: NextResponse, req: NextRequest): void;
|
|
23
|
+
/** Extract session token from request cookies (tries both cookie names). */
|
|
24
|
+
declare function getSessionToken(req: NextRequest): string;
|
|
25
|
+
/** Check if a value is a session-expired error code. */
|
|
26
|
+
declare function isSessionExpiredCode(value: unknown): boolean;
|
|
27
|
+
/** Detect if a 403 response indicates session expiry (reads response body). */
|
|
28
|
+
declare function shouldClearSessionFromForbidden(response: Response): Promise<boolean>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* CSRF protection and CORS header management for the API proxy.
|
|
32
|
+
*
|
|
33
|
+
* Validates request origins against configured allowed origins,
|
|
34
|
+
* applies CORS headers to responses, and enforces CSRF protection
|
|
35
|
+
* for state-changing HTTP methods.
|
|
36
|
+
*
|
|
37
|
+
* @module handlers/csrf-cors
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Normalize a URL string to its origin (scheme + host + port). */
|
|
41
|
+
declare function normalizeOrigin(value: string): string | null;
|
|
42
|
+
/** Check if the request comes from the configured FRONTEND_IP. */
|
|
43
|
+
declare function isInternalIpAccess(req: NextRequest): boolean;
|
|
44
|
+
/** Resolve the allowed origin for a request (checks FRONTEND_URL and FRONTEND_IP). */
|
|
45
|
+
declare function resolveAllowedOrigin(req: NextRequest): string | null;
|
|
46
|
+
/** Check if a request should be rejected by CSRF protection. */
|
|
47
|
+
declare function shouldRejectByCsrfProtection(req: NextRequest, method: string, pathKey: string): boolean;
|
|
48
|
+
/** Apply CORS headers to a response. */
|
|
49
|
+
declare function applyCorsHeaders(headers: Headers, req: NextRequest, isIpAccess: boolean): void;
|
|
50
|
+
/** Check if CSRF exempt paths are configured (disallowed in production). */
|
|
51
|
+
declare function hasCsrfExemptPaths(): boolean;
|
|
52
|
+
/** Validate production security config. Returns error string or null. */
|
|
53
|
+
declare function resolveProdSecurityConfigError(): string | null;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Login rate limiting logic for the API proxy.
|
|
57
|
+
*
|
|
58
|
+
* Uses the pluggable RateLimiterAdapter from rate-limiter.ts so multi-instance
|
|
59
|
+
* deployments can swap in a shared-storage adapter.
|
|
60
|
+
*
|
|
61
|
+
* @module handlers/rate-limit-handler
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
interface RateLimitKeys {
|
|
65
|
+
pairKey: string;
|
|
66
|
+
accountKey: string | null;
|
|
67
|
+
}
|
|
68
|
+
/** Get client IP address from request headers or connection. */
|
|
69
|
+
declare function getClientAddress(request: NextRequest): string;
|
|
70
|
+
/** Build rate limit keys from request and username. */
|
|
71
|
+
declare function getLoginRateLimitKeys(request: NextRequest, username: string): RateLimitKeys;
|
|
72
|
+
/** Remove expired rate limit entries from adapter stores. */
|
|
73
|
+
declare function cleanupExpiredLoginLimits(now: number): void;
|
|
74
|
+
/** Get remaining block time in ms (0 = not blocked). */
|
|
75
|
+
declare function getRateLimitRetryAfterMs(keys: RateLimitKeys, now: number): number;
|
|
76
|
+
/** Record a failed login attempt for rate limiting. */
|
|
77
|
+
declare function registerFailedLoginAttempt(keys: RateLimitKeys, _now: number): void;
|
|
78
|
+
/** Clear rate limit state for given keys (after successful login). */
|
|
79
|
+
declare function clearLoginAttemptState(keys: RateLimitKeys): void;
|
|
80
|
+
|
|
81
|
+
export { type RateLimitKeys, applyCorsHeaders, cleanupExpiredLoginLimits, clearLoginAttemptState, clearSessionCookie, getClientAddress, getLoginRateLimitKeys, getRateLimitRetryAfterMs, getSessionToken, hasCsrfExemptPaths, isInternalIpAccess, isLocalHostRequest, isSessionExpiredCode, normalizeOrigin, registerFailedLoginAttempt, resolveAllowedOrigin, resolveProdSecurityConfigError, resolveSessionCookieName, setSessionCookie, shouldClearSessionFromForbidden, shouldRejectByCsrfProtection, useSecureCookies };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyCorsHeaders,
|
|
3
|
+
cleanupExpiredLoginLimits,
|
|
4
|
+
clearLoginAttemptState,
|
|
5
|
+
clearSessionCookie,
|
|
6
|
+
getClientAddress,
|
|
7
|
+
getLoginRateLimitKeys,
|
|
8
|
+
getRateLimitRetryAfterMs,
|
|
9
|
+
getSessionToken,
|
|
10
|
+
hasCsrfExemptPaths,
|
|
11
|
+
isInternalIpAccess,
|
|
12
|
+
isLocalHostRequest,
|
|
13
|
+
isSessionExpiredCode,
|
|
14
|
+
normalizeOrigin,
|
|
15
|
+
registerFailedLoginAttempt,
|
|
16
|
+
resolveAllowedOrigin,
|
|
17
|
+
resolveProdSecurityConfigError,
|
|
18
|
+
resolveSessionCookieName,
|
|
19
|
+
setSessionCookie,
|
|
20
|
+
shouldClearSessionFromForbidden,
|
|
21
|
+
shouldRejectByCsrfProtection,
|
|
22
|
+
useSecureCookies
|
|
23
|
+
} from "../chunk-FEB3UZEG.js";
|
|
24
|
+
import "../chunk-7IUSTA5W.js";
|
|
25
|
+
export {
|
|
26
|
+
applyCorsHeaders,
|
|
27
|
+
cleanupExpiredLoginLimits,
|
|
28
|
+
clearLoginAttemptState,
|
|
29
|
+
clearSessionCookie,
|
|
30
|
+
getClientAddress,
|
|
31
|
+
getLoginRateLimitKeys,
|
|
32
|
+
getRateLimitRetryAfterMs,
|
|
33
|
+
getSessionToken,
|
|
34
|
+
hasCsrfExemptPaths,
|
|
35
|
+
isInternalIpAccess,
|
|
36
|
+
isLocalHostRequest,
|
|
37
|
+
isSessionExpiredCode,
|
|
38
|
+
normalizeOrigin,
|
|
39
|
+
registerFailedLoginAttempt,
|
|
40
|
+
resolveAllowedOrigin,
|
|
41
|
+
resolveProdSecurityConfigError,
|
|
42
|
+
resolveSessionCookieName,
|
|
43
|
+
setSessionCookie,
|
|
44
|
+
shouldClearSessionFromForbidden,
|
|
45
|
+
shouldRejectByCsrfProtection,
|
|
46
|
+
useSecureCookies
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { DELETE, GET, OPTIONS, PATCH, POST, PUT } from './api-route-handler.js';
|
|
2
|
+
export { proxy, proxyConfig } from './proxy-middleware.js';
|
|
3
|
+
export { RuntimeEnvScript, runtimeEnvGET } from './runtime-env.js';
|
|
4
|
+
export { BASE_SECURITY_HEADERS, BASE_SECURITY_HEADER_VALUES, PERMISSIONS_POLICY_VALUE } from './security-headers.js';
|
|
5
|
+
import 'next/server.js';
|
|
6
|
+
import 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
declare const SPRING_SERVER_VERSION: string;
|
|
9
|
+
|
|
10
|
+
export { SPRING_SERVER_VERSION };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
proxy,
|
|
3
|
+
proxyConfig
|
|
4
|
+
} from "./chunk-OYTV4D7E.js";
|
|
5
|
+
import {
|
|
6
|
+
DELETE,
|
|
7
|
+
GET,
|
|
8
|
+
OPTIONS,
|
|
9
|
+
PATCH,
|
|
10
|
+
POST,
|
|
11
|
+
PUT
|
|
12
|
+
} from "./chunk-CLZU34DG.js";
|
|
13
|
+
import "./chunk-FEB3UZEG.js";
|
|
14
|
+
import {
|
|
15
|
+
GET as GET2,
|
|
16
|
+
RuntimeEnvScript
|
|
17
|
+
} from "./chunk-YV6DZVPI.js";
|
|
18
|
+
import "./chunk-7IUSTA5W.js";
|
|
19
|
+
import {
|
|
20
|
+
BASE_SECURITY_HEADERS,
|
|
21
|
+
BASE_SECURITY_HEADER_VALUES,
|
|
22
|
+
PERMISSIONS_POLICY_VALUE
|
|
23
|
+
} from "./chunk-KA7RJCWA.js";
|
|
24
|
+
|
|
25
|
+
// src/index.ts
|
|
26
|
+
import "server-only";
|
|
27
|
+
var SPRING_SERVER_VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
28
|
+
export {
|
|
29
|
+
BASE_SECURITY_HEADERS,
|
|
30
|
+
BASE_SECURITY_HEADER_VALUES,
|
|
31
|
+
DELETE,
|
|
32
|
+
GET,
|
|
33
|
+
OPTIONS,
|
|
34
|
+
PATCH,
|
|
35
|
+
PERMISSIONS_POLICY_VALUE,
|
|
36
|
+
POST,
|
|
37
|
+
PUT,
|
|
38
|
+
RuntimeEnvScript,
|
|
39
|
+
SPRING_SERVER_VERSION,
|
|
40
|
+
proxy,
|
|
41
|
+
proxyConfig,
|
|
42
|
+
GET2 as runtimeEnvGET
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @spring-systems/server\n// Next.js server-only code: proxy middleware, API route handler, runtime env injection.\n// Use sub-path imports: @spring-systems/server/proxy, /api-handler, /runtime-env\nimport \"server-only\";\n\ndeclare const __VERSION__: string;\nexport const SPRING_SERVER_VERSION: string = typeof __VERSION__ !== \"undefined\" ? __VERSION__ : \"0.0.0-dev\";\n\nexport { DELETE, GET, OPTIONS, PATCH, POST, PUT } from \"./api-route-handler\";\nexport { proxy, proxyConfig } from \"./proxy-middleware\";\nexport { runtimeEnvGET, RuntimeEnvScript } from \"./runtime-env\";\nexport { BASE_SECURITY_HEADER_VALUES, BASE_SECURITY_HEADERS, PERMISSIONS_POLICY_VALUE } from \"./security-headers\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAGA,OAAO;AAGA,IAAM,wBAAgC,OAAqC,UAAc;","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { UIAdapter, RouteAdapter } from '@spring-systems/ui/adapters';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Next.js adapter presets for RouteAdapter and UIAdapter.
|
|
5
|
+
* Consumer app calls these in Root.tsx / layout.tsx to register the Next.js implementations.
|
|
6
|
+
* @module next-adapters
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface NextUIAdapterOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Default SSR behavior for `dynamic` when call-site does not provide `{ ssr }`.
|
|
12
|
+
* Defaults to `true` for backward compatibility.
|
|
13
|
+
*/
|
|
14
|
+
defaultDynamicSsr?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Creates a RouteAdapter backed by Next.js App Router hooks. */
|
|
17
|
+
declare function createNextRouteAdapter(): RouteAdapter;
|
|
18
|
+
/** Creates a UIAdapter backed by Next.js Image, Link, and dynamic imports. */
|
|
19
|
+
declare function createNextUIAdapter(options?: NextUIAdapterOptions): UIAdapter;
|
|
20
|
+
/** Creates a UIAdapter preset optimized for client-only apps (dynamic imports default to `ssr: false`). */
|
|
21
|
+
declare function createClientOnlyNextUIAdapter(): UIAdapter;
|
|
22
|
+
/** Creates a UIAdapter preset with SSR-enabled dynamic imports by default. */
|
|
23
|
+
declare function createSsrNextUIAdapter(): UIAdapter;
|
|
24
|
+
|
|
25
|
+
export { type NextUIAdapterOptions, createClientOnlyNextUIAdapter, createNextRouteAdapter, createNextUIAdapter, createSsrNextUIAdapter };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
createClientOnlyNextUIAdapter,
|
|
4
|
+
createNextRouteAdapter,
|
|
5
|
+
createNextUIAdapter,
|
|
6
|
+
createSsrNextUIAdapter
|
|
7
|
+
} from "./chunk-CP33WQ5Q.js";
|
|
8
|
+
export {
|
|
9
|
+
createClientOnlyNextUIAdapter,
|
|
10
|
+
createNextRouteAdapter,
|
|
11
|
+
createNextUIAdapter,
|
|
12
|
+
createSsrNextUIAdapter
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=next-adapters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable rate limiter for API proxy authentication.
|
|
3
|
+
*
|
|
4
|
+
* The default implementation uses in-memory Maps (suitable for single-instance deployments).
|
|
5
|
+
* Multi-instance deployments can replace this with a custom adapter via `setRateLimiterAdapter()`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { setRateLimiterAdapter } from "@spring-systems/server/rate-limiter";
|
|
10
|
+
* import { createCustomRateLimiter } from "./my-rate-limiter";
|
|
11
|
+
*
|
|
12
|
+
* setRateLimiterAdapter(createCustomRateLimiter());
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @module rate-limiter
|
|
16
|
+
*/
|
|
17
|
+
interface RateLimitEntry {
|
|
18
|
+
count: number;
|
|
19
|
+
windowStartedAt: number;
|
|
20
|
+
blockedUntil: number;
|
|
21
|
+
}
|
|
22
|
+
interface RateLimitPolicy {
|
|
23
|
+
windowMs: number;
|
|
24
|
+
blockMs: number;
|
|
25
|
+
maxAttemptsByIpAndAccount: number;
|
|
26
|
+
maxAttemptsByAccount: number;
|
|
27
|
+
maxKeys: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Rate limiter adapter interface. Implementations must be async-safe.
|
|
31
|
+
*/
|
|
32
|
+
interface RateLimiterAdapter {
|
|
33
|
+
/** Get the current rate limit entry for a key, or null if no entry exists. */
|
|
34
|
+
get(store: "ip" | "account", key: string): RateLimitEntry | null;
|
|
35
|
+
/** Set/update the rate limit entry for a key. */
|
|
36
|
+
set(store: "ip" | "account", key: string, entry: RateLimitEntry): void;
|
|
37
|
+
/** Delete an entry (e.g. on successful login). */
|
|
38
|
+
delete(store: "ip" | "account", key: string): void;
|
|
39
|
+
/** Get the total number of tracked keys (for eviction logic). */
|
|
40
|
+
size(store: "ip" | "account"): number;
|
|
41
|
+
/** Clear the oldest entries when maxKeys is exceeded. */
|
|
42
|
+
evictOldest(store: "ip" | "account", count: number): void;
|
|
43
|
+
/**
|
|
44
|
+
* Remove expired entries for a store.
|
|
45
|
+
* Optional to preserve compatibility with existing custom adapters.
|
|
46
|
+
*/
|
|
47
|
+
sweepExpired?(store: "ip" | "account", now: number, windowMs: number): void;
|
|
48
|
+
}
|
|
49
|
+
/** Replace the default in-memory rate limiter with a custom adapter. */
|
|
50
|
+
declare function setRateLimiterAdapter(custom: RateLimiterAdapter): void;
|
|
51
|
+
/** Get the current rate limiter adapter. */
|
|
52
|
+
declare function getRateLimiterAdapter(): RateLimiterAdapter;
|
|
53
|
+
/**
|
|
54
|
+
* Check if a login attempt should be rate-limited.
|
|
55
|
+
* @returns A reason string if blocked, or null if allowed.
|
|
56
|
+
*/
|
|
57
|
+
declare function checkRateLimit(ipKey: string, accountKey: string | null, policy: RateLimitPolicy): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* Record a failed login attempt.
|
|
60
|
+
*/
|
|
61
|
+
declare function recordFailedAttempt(ipKey: string, accountKey: string | null, policy: RateLimitPolicy): void;
|
|
62
|
+
/**
|
|
63
|
+
* Clear rate limit entries for a key (e.g. on successful login).
|
|
64
|
+
*/
|
|
65
|
+
declare function clearRateLimitEntries(ipKey: string, accountKey: string | null): void;
|
|
66
|
+
|
|
67
|
+
export { type RateLimitEntry, type RateLimitPolicy, type RateLimiterAdapter, checkRateLimit, clearRateLimitEntries, getRateLimiterAdapter, recordFailedAttempt, setRateLimiterAdapter };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkRateLimit,
|
|
3
|
+
clearRateLimitEntries,
|
|
4
|
+
getRateLimiterAdapter,
|
|
5
|
+
recordFailedAttempt,
|
|
6
|
+
setRateLimiterAdapter
|
|
7
|
+
} from "./chunk-7IUSTA5W.js";
|
|
8
|
+
export {
|
|
9
|
+
checkRateLimit,
|
|
10
|
+
clearRateLimitEntries,
|
|
11
|
+
getRateLimiterAdapter,
|
|
12
|
+
recordFailedAttempt,
|
|
13
|
+
setRateLimiterAdapter
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server.js';
|
|
2
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
declare function GET(): Promise<NextResponse<{
|
|
5
|
+
error: string;
|
|
6
|
+
}> | NextResponse<{
|
|
7
|
+
[k: string]: string | undefined;
|
|
8
|
+
}>>;
|
|
9
|
+
|
|
10
|
+
interface RuntimeEnvScriptProps {
|
|
11
|
+
nonce?: string;
|
|
12
|
+
}
|
|
13
|
+
declare function RuntimeEnvScript({ nonce }: RuntimeEnvScriptProps): react_jsx_runtime.JSX.Element;
|
|
14
|
+
|
|
15
|
+
export { RuntimeEnvScript, type RuntimeEnvScriptProps, GET as runtimeEnvGET };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare const PERMISSIONS_POLICY_VALUE = "accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), browsing-topics=()";
|
|
2
|
+
declare const BASE_SECURITY_HEADER_VALUES: Readonly<Record<string, string>>;
|
|
3
|
+
declare const BASE_SECURITY_HEADERS: {
|
|
4
|
+
key: string;
|
|
5
|
+
value: string;
|
|
6
|
+
}[];
|
|
7
|
+
|
|
8
|
+
export { BASE_SECURITY_HEADERS, BASE_SECURITY_HEADER_VALUES, PERMISSIONS_POLICY_VALUE };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BASE_SECURITY_HEADERS,
|
|
3
|
+
BASE_SECURITY_HEADER_VALUES,
|
|
4
|
+
PERMISSIONS_POLICY_VALUE
|
|
5
|
+
} from "./chunk-KA7RJCWA.js";
|
|
6
|
+
export {
|
|
7
|
+
BASE_SECURITY_HEADERS,
|
|
8
|
+
BASE_SECURITY_HEADER_VALUES,
|
|
9
|
+
PERMISSIONS_POLICY_VALUE
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=security-headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spring-systems/server",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Next.js server-only code for Spring Systems SPRING (proxy, API routes, runtime env)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE",
|
|
12
|
+
"CHANGELOG.md"
|
|
13
|
+
],
|
|
14
|
+
"license": "UNLICENSED",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://martin-zadak@bitbucket.org/springsystems-projects/spring-framework-frontend.git",
|
|
18
|
+
"directory": "packages/server"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://bitbucket.org/springsystems-projects/spring-framework-frontend/src/main/packages/server#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://bitbucket.org/springsystems-projects/spring-framework-frontend/issues"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"spring",
|
|
26
|
+
"nextjs",
|
|
27
|
+
"server",
|
|
28
|
+
"proxy",
|
|
29
|
+
"api-routes",
|
|
30
|
+
"runtime-env",
|
|
31
|
+
"security"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22.0.0"
|
|
35
|
+
},
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.js",
|
|
41
|
+
"default": "./dist/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./proxy": {
|
|
44
|
+
"types": "./dist/proxy-middleware.d.ts",
|
|
45
|
+
"import": "./dist/proxy-middleware.js",
|
|
46
|
+
"default": "./dist/proxy-middleware.js"
|
|
47
|
+
},
|
|
48
|
+
"./api-handler": {
|
|
49
|
+
"types": "./dist/api-route-handler.d.ts",
|
|
50
|
+
"import": "./dist/api-route-handler.js",
|
|
51
|
+
"default": "./dist/api-route-handler.js"
|
|
52
|
+
},
|
|
53
|
+
"./runtime-env": {
|
|
54
|
+
"types": "./dist/runtime-env.d.ts",
|
|
55
|
+
"import": "./dist/runtime-env.js",
|
|
56
|
+
"default": "./dist/runtime-env.js"
|
|
57
|
+
},
|
|
58
|
+
"./client": {
|
|
59
|
+
"types": "./dist/client.d.ts",
|
|
60
|
+
"import": "./dist/client.js",
|
|
61
|
+
"default": "./dist/client.js"
|
|
62
|
+
},
|
|
63
|
+
"./adapters": {
|
|
64
|
+
"types": "./dist/next-adapters.d.ts",
|
|
65
|
+
"import": "./dist/next-adapters.js",
|
|
66
|
+
"default": "./dist/next-adapters.js"
|
|
67
|
+
},
|
|
68
|
+
"./rate-limiter": {
|
|
69
|
+
"types": "./dist/rate-limiter.d.ts",
|
|
70
|
+
"import": "./dist/rate-limiter.js",
|
|
71
|
+
"default": "./dist/rate-limiter.js"
|
|
72
|
+
},
|
|
73
|
+
"./handlers": {
|
|
74
|
+
"types": "./dist/handlers/index.d.ts",
|
|
75
|
+
"import": "./dist/handlers/index.js",
|
|
76
|
+
"default": "./dist/handlers/index.js"
|
|
77
|
+
},
|
|
78
|
+
"./security-headers": {
|
|
79
|
+
"types": "./dist/security-headers.d.ts",
|
|
80
|
+
"import": "./dist/security-headers.js",
|
|
81
|
+
"default": "./dist/security-headers.js"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"scripts": {
|
|
85
|
+
"build": "tsup",
|
|
86
|
+
"dev": "tsup --watch",
|
|
87
|
+
"typecheck": "tsc --noEmit",
|
|
88
|
+
"test": "vitest run",
|
|
89
|
+
"check:exports": "node scripts/check-exports.mjs",
|
|
90
|
+
"prepack": "pnpm run build && pnpm check:exports",
|
|
91
|
+
"check:circular": "madge --circular --extensions ts,tsx --ts-config ../../tsconfig.base.json ./src",
|
|
92
|
+
"lint": "eslint src/"
|
|
93
|
+
},
|
|
94
|
+
"peerDependencies": {
|
|
95
|
+
"@spring-systems/core": "^0.8.0",
|
|
96
|
+
"@spring-systems/ui": "^0.8.0",
|
|
97
|
+
"next": "^16.0.0",
|
|
98
|
+
"react": "^19.0.0"
|
|
99
|
+
},
|
|
100
|
+
"dependencies": {
|
|
101
|
+
"server-only": "^0.0.1"
|
|
102
|
+
},
|
|
103
|
+
"devDependencies": {
|
|
104
|
+
"@spring-systems/core": "workspace:^",
|
|
105
|
+
"@spring-systems/ui": "workspace:^",
|
|
106
|
+
"@types/react": "^19.2.14",
|
|
107
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
108
|
+
"next": "^16.1.6",
|
|
109
|
+
"react": "^19.2.4",
|
|
110
|
+
"tsup": "^8.5.1",
|
|
111
|
+
"typescript": "^5.9.3",
|
|
112
|
+
"vitest": "^4.0.18"
|
|
113
|
+
}
|
|
114
|
+
}
|