@sigitex/route 1.0.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/LICENSE +7 -0
- package/README.md +7 -0
- package/package.json +47 -0
- package/src/Assets.ts +4 -0
- package/src/CSP.ts +18 -0
- package/src/HTTP.ts +83 -0
- package/src/Router.ts +100 -0
- package/src/RouterError.ts +31 -0
- package/src/bun/bun.ts +37 -0
- package/src/bun/index.ts +1 -0
- package/src/bun/tsconfig.json +8 -0
- package/src/cloudflare/cloudflare.ts +43 -0
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/tsconfig.json +8 -0
- package/src/env.d.ts +1 -0
- package/src/global.d.ts +1 -0
- package/src/handler/app.ts +27 -0
- package/src/handler/assets.ts +10 -0
- package/src/handler/index.ts +7 -0
- package/src/handler/mount.ts +13 -0
- package/src/handler/noop.ts +3 -0
- package/src/handler/pattern.ts +71 -0
- package/src/handler/prefix.ts +32 -0
- package/src/handler/use.ts +36 -0
- package/src/index.ts +9 -0
- package/src/middleware/bodyLimit.ts +43 -0
- package/src/middleware/cache.ts +53 -0
- package/src/middleware/cookies.ts +76 -0
- package/src/middleware/cors.ts +113 -0
- package/src/middleware/csp.ts +109 -0
- package/src/middleware/csrf.ts +66 -0
- package/src/middleware/filter.ts +10 -0
- package/src/middleware/frameGuard.ts +15 -0
- package/src/middleware/hardened.ts +43 -0
- package/src/middleware/hsts.ts +24 -0
- package/src/middleware/https.ts +9 -0
- package/src/middleware/index.ts +17 -0
- package/src/middleware/noSniff.ts +11 -0
- package/src/middleware/rateLimit.ts +107 -0
- package/src/middleware/referrerPolicy.ts +27 -0
- package/src/middleware/requestId.ts +31 -0
- package/src/middleware/setHeader.ts +7 -0
- package/src/middleware/www.ts +12 -0
- package/src/route.ts +21 -0
- package/src/router.types.ts +48 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { HTTP } from "../HTTP"
|
|
2
|
+
import type { RequestContext, RouteMiddleware } from "../router.types"
|
|
3
|
+
|
|
4
|
+
export type BodyLimitOptions = {
|
|
5
|
+
readonly maxSize?: number
|
|
6
|
+
readonly contentTypes?: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function bodyLimit(options?: BodyLimitOptions): RouteMiddleware {
|
|
10
|
+
const maxSize = options?.maxSize ?? 1_048_576
|
|
11
|
+
const contentTypes = options?.contentTypes
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
before: ({ request }: RequestContext) => {
|
|
15
|
+
const contentLength = request.headers.get(HTTP.header.ContentLength)
|
|
16
|
+
if (contentLength && Number.parseInt(contentLength, 10) > maxSize) {
|
|
17
|
+
return new Response(
|
|
18
|
+
JSON.stringify({ error: HTTP.statusText.PayloadTooLarge }),
|
|
19
|
+
{
|
|
20
|
+
status: HTTP.status.PayloadTooLarge,
|
|
21
|
+
statusText: HTTP.statusText.PayloadTooLarge,
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (contentTypes) {
|
|
27
|
+
const contentType = request.headers.get(HTTP.header.ContentType)
|
|
28
|
+
if (
|
|
29
|
+
contentType &&
|
|
30
|
+
!contentTypes.some((allowed) => contentType.startsWith(allowed))
|
|
31
|
+
) {
|
|
32
|
+
return new Response(
|
|
33
|
+
JSON.stringify({ error: HTTP.statusText.UnsupportedMediaType }),
|
|
34
|
+
{
|
|
35
|
+
status: HTTP.status.UnsupportedMediaType,
|
|
36
|
+
statusText: HTTP.statusText.UnsupportedMediaType,
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// oxlint-disable no-eq-null -- fix this
|
|
2
|
+
// oxlint-disable curly
|
|
3
|
+
import { HTTP } from "../HTTP"
|
|
4
|
+
import type { ResponseContext, RouteMiddleware } from "../router.types"
|
|
5
|
+
|
|
6
|
+
export type CacheOptions = {
|
|
7
|
+
readonly public?: boolean
|
|
8
|
+
readonly private?: boolean
|
|
9
|
+
readonly maxAge?: number
|
|
10
|
+
readonly sMaxAge?: number
|
|
11
|
+
readonly noCache?: boolean
|
|
12
|
+
readonly noStore?: boolean
|
|
13
|
+
readonly mustRevalidate?: boolean
|
|
14
|
+
readonly proxyRevalidate?: boolean
|
|
15
|
+
readonly immutable?: boolean
|
|
16
|
+
readonly staleWhileRevalidate?: number
|
|
17
|
+
readonly staleIfError?: number
|
|
18
|
+
readonly vary?: string | string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cache(options: string | CacheOptions): RouteMiddleware {
|
|
22
|
+
const directive =
|
|
23
|
+
typeof options === "string" ? options : buildDirective(options)
|
|
24
|
+
const vary = typeof options === "object" ? options.vary : undefined
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
after: ({ response }: ResponseContext) => {
|
|
28
|
+
response.headers.set(HTTP.header.CacheControl, directive)
|
|
29
|
+
if (vary) {
|
|
30
|
+
const value = Array.isArray(vary) ? vary.join(", ") : vary
|
|
31
|
+
response.headers.set(HTTP.header.Vary, value)
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildDirective(options: CacheOptions): string {
|
|
38
|
+
const parts: string[] = []
|
|
39
|
+
if (options.public) parts.push("public")
|
|
40
|
+
if (options.private) parts.push("private")
|
|
41
|
+
if (options.noCache) parts.push("no-cache")
|
|
42
|
+
if (options.noStore) parts.push("no-store")
|
|
43
|
+
if (options.mustRevalidate) parts.push("must-revalidate")
|
|
44
|
+
if (options.proxyRevalidate) parts.push("proxy-revalidate")
|
|
45
|
+
if (options.immutable) parts.push("immutable")
|
|
46
|
+
if (options.maxAge != null) parts.push(`max-age=${options.maxAge}`)
|
|
47
|
+
if (options.sMaxAge != null) parts.push(`s-maxage=${options.sMaxAge}`)
|
|
48
|
+
if (options.staleWhileRevalidate != null)
|
|
49
|
+
parts.push(`stale-while-revalidate=${options.staleWhileRevalidate}`)
|
|
50
|
+
if (options.staleIfError != null)
|
|
51
|
+
parts.push(`stale-if-error=${options.staleIfError}`)
|
|
52
|
+
return parts.join(", ")
|
|
53
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RequestContext,
|
|
3
|
+
ResponseContext,
|
|
4
|
+
RouteMiddleware,
|
|
5
|
+
} from "../router.types"
|
|
6
|
+
|
|
7
|
+
export type CookieOptions = {
|
|
8
|
+
domain?: string
|
|
9
|
+
expires?: Date
|
|
10
|
+
httpOnly?: boolean
|
|
11
|
+
maxAge?: number
|
|
12
|
+
path?: string
|
|
13
|
+
sameSite?: "strict" | "lax" | "none"
|
|
14
|
+
secure?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type Cookies = {
|
|
18
|
+
get(name: string): string | undefined
|
|
19
|
+
set(name: string, value: string, options?: CookieOptions): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function cookies(): RouteMiddleware {
|
|
23
|
+
const pending: { name: string; value: string; options?: CookieOptions }[] = []
|
|
24
|
+
let parsed: Record<string, string> = {}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
before: ({ request, bind }: RequestContext) => {
|
|
28
|
+
parsed = parseCookies(request.headers.get("cookie") ?? "")
|
|
29
|
+
bind({
|
|
30
|
+
cookies: {
|
|
31
|
+
get: (name: string) => parsed[name],
|
|
32
|
+
set: (name: string, value: string, options?: CookieOptions) =>
|
|
33
|
+
pending.push({ name, value, options }),
|
|
34
|
+
} satisfies Cookies,
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
after: ({ response }: ResponseContext) => {
|
|
38
|
+
for (const { name, value, options } of pending) {
|
|
39
|
+
response.headers.append(
|
|
40
|
+
"set-cookie",
|
|
41
|
+
serializeCookie(name, value, options),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseCookies(header: string): Record<string, string> {
|
|
49
|
+
const result: Record<string, string> = {}
|
|
50
|
+
for (const pair of header.split(";")) {
|
|
51
|
+
const index = pair.indexOf("=")
|
|
52
|
+
if (index === -1) continue
|
|
53
|
+
const key = pair.slice(0, index).trim()
|
|
54
|
+
const value = pair.slice(index + 1).trim()
|
|
55
|
+
if (key) result[key] = decodeURIComponent(value)
|
|
56
|
+
}
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function serializeCookie(
|
|
61
|
+
name: string,
|
|
62
|
+
value: string,
|
|
63
|
+
options?: CookieOptions,
|
|
64
|
+
): string {
|
|
65
|
+
let cookie = `${name}=${encodeURIComponent(value)}`
|
|
66
|
+
if (!options) return cookie
|
|
67
|
+
if (options.domain) cookie += `; Domain=${options.domain}`
|
|
68
|
+
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`
|
|
69
|
+
if (options.httpOnly) cookie += "; HttpOnly"
|
|
70
|
+
// oxlint-disable-next-line no-eq-null
|
|
71
|
+
if (options.maxAge != null) cookie += `; Max-Age=${options.maxAge}`
|
|
72
|
+
if (options.path) cookie += `; Path=${options.path}`
|
|
73
|
+
if (options.sameSite) cookie += `; SameSite=${options.sameSite}`
|
|
74
|
+
if (options.secure) cookie += "; Secure"
|
|
75
|
+
return cookie
|
|
76
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { HTTP } from "../HTTP"
|
|
2
|
+
import type {
|
|
3
|
+
RequestContext,
|
|
4
|
+
ResponseContext,
|
|
5
|
+
RouteMiddleware,
|
|
6
|
+
} from "../router.types"
|
|
7
|
+
|
|
8
|
+
export type CorsOptions = {
|
|
9
|
+
readonly origin?: string | string[] | ((origin: string) => boolean)
|
|
10
|
+
readonly methods?: string[]
|
|
11
|
+
readonly allowHeaders?: string[]
|
|
12
|
+
readonly exposeHeaders?: string[]
|
|
13
|
+
readonly credentials?: boolean
|
|
14
|
+
readonly maxAge?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function cors(options?: CorsOptions): RouteMiddleware {
|
|
18
|
+
const originOption = options?.origin ?? "*"
|
|
19
|
+
const methods = options?.methods ?? [
|
|
20
|
+
HTTP.method.GET,
|
|
21
|
+
HTTP.method.HEAD,
|
|
22
|
+
HTTP.method.PUT,
|
|
23
|
+
HTTP.method.PATCH,
|
|
24
|
+
HTTP.method.POST,
|
|
25
|
+
HTTP.method.DELETE,
|
|
26
|
+
]
|
|
27
|
+
const allowHeaders = options?.allowHeaders
|
|
28
|
+
const exposeHeaders = options?.exposeHeaders
|
|
29
|
+
const credentials = options?.credentials ?? false
|
|
30
|
+
const maxAge = options?.maxAge
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
before: ({ request }: RequestContext) => {
|
|
34
|
+
if (request.method !== HTTP.method.OPTIONS) return
|
|
35
|
+
|
|
36
|
+
const requestOrigin = request.headers.get(HTTP.header.Origin)
|
|
37
|
+
if (!requestOrigin) return
|
|
38
|
+
|
|
39
|
+
const response = new Response(null, { status: HTTP.status.NoContent })
|
|
40
|
+
setOriginHeader(response, requestOrigin, originOption)
|
|
41
|
+
response.headers.set(
|
|
42
|
+
HTTP.header.AccessControlAllowMethods,
|
|
43
|
+
methods.join(", "),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const requestedHeaders = request.headers.get(
|
|
47
|
+
HTTP.header.AccessControlRequestHeaders,
|
|
48
|
+
)
|
|
49
|
+
if (allowHeaders) {
|
|
50
|
+
response.headers.set(
|
|
51
|
+
HTTP.header.AccessControlAllowHeaders,
|
|
52
|
+
allowHeaders.join(", "),
|
|
53
|
+
)
|
|
54
|
+
} else if (requestedHeaders) {
|
|
55
|
+
response.headers.set(
|
|
56
|
+
HTTP.header.AccessControlAllowHeaders,
|
|
57
|
+
requestedHeaders,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (credentials) {
|
|
62
|
+
response.headers.set(HTTP.header.AccessControlAllowCredentials, "true")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// oxlint-disable-next-line no-eq-null -- fix this
|
|
66
|
+
if (maxAge != null) {
|
|
67
|
+
response.headers.set(HTTP.header.AccessControlMaxAge, String(maxAge))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return response
|
|
71
|
+
},
|
|
72
|
+
after: ({ request, response }: ResponseContext) => {
|
|
73
|
+
const requestOrigin = request.headers.get(HTTP.header.Origin)
|
|
74
|
+
if (!requestOrigin) return
|
|
75
|
+
|
|
76
|
+
setOriginHeader(response, requestOrigin, originOption)
|
|
77
|
+
|
|
78
|
+
if (credentials) {
|
|
79
|
+
response.headers.set(HTTP.header.AccessControlAllowCredentials, "true")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (exposeHeaders) {
|
|
83
|
+
response.headers.set(
|
|
84
|
+
HTTP.header.AccessControlExposeHeaders,
|
|
85
|
+
exposeHeaders.join(", "),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function setOriginHeader(
|
|
93
|
+
response: Response,
|
|
94
|
+
requestOrigin: string,
|
|
95
|
+
originOption: string | string[] | ((origin: string) => boolean),
|
|
96
|
+
) {
|
|
97
|
+
if (originOption === "*") {
|
|
98
|
+
response.headers.set(HTTP.header.AccessControlAllowOrigin, "*")
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const allowed =
|
|
103
|
+
typeof originOption === "function"
|
|
104
|
+
? originOption(requestOrigin)
|
|
105
|
+
: typeof originOption === "string"
|
|
106
|
+
? originOption === requestOrigin
|
|
107
|
+
: originOption.includes(requestOrigin)
|
|
108
|
+
|
|
109
|
+
if (allowed) {
|
|
110
|
+
response.headers.set(HTTP.header.AccessControlAllowOrigin, requestOrigin)
|
|
111
|
+
response.headers.append(HTTP.header.Vary, HTTP.header.Origin)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { CSP, type CspSource } from "../CSP"
|
|
2
|
+
import { HTTP } from "../HTTP"
|
|
3
|
+
import type {
|
|
4
|
+
RequestContext,
|
|
5
|
+
ResponseContext,
|
|
6
|
+
RouteMiddleware,
|
|
7
|
+
} from "../router.types"
|
|
8
|
+
|
|
9
|
+
export type CspOptions = {
|
|
10
|
+
readonly defaultSrc?: CspSource[]
|
|
11
|
+
readonly scriptSrc?: CspSource[]
|
|
12
|
+
readonly styleSrc?: CspSource[]
|
|
13
|
+
readonly imgSrc?: CspSource[]
|
|
14
|
+
readonly connectSrc?: CspSource[]
|
|
15
|
+
readonly fontSrc?: CspSource[]
|
|
16
|
+
readonly frameSrc?: CspSource[]
|
|
17
|
+
readonly frameAncestors?: CspSource[]
|
|
18
|
+
readonly mediaSrc?: CspSource[]
|
|
19
|
+
readonly objectSrc?: CspSource[]
|
|
20
|
+
readonly workerSrc?: CspSource[]
|
|
21
|
+
readonly childSrc?: CspSource[]
|
|
22
|
+
readonly baseUri?: CspSource[]
|
|
23
|
+
readonly formAction?: CspSource[]
|
|
24
|
+
readonly manifestSrc?: CspSource[]
|
|
25
|
+
readonly upgradeInsecureRequests?: boolean
|
|
26
|
+
readonly reportOnly?: boolean
|
|
27
|
+
readonly reportTo?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const directiveMap: Record<string, string> = {
|
|
31
|
+
defaultSrc: "default-src",
|
|
32
|
+
scriptSrc: "script-src",
|
|
33
|
+
styleSrc: "style-src",
|
|
34
|
+
imgSrc: "img-src",
|
|
35
|
+
connectSrc: "connect-src",
|
|
36
|
+
fontSrc: "font-src",
|
|
37
|
+
frameSrc: "frame-src",
|
|
38
|
+
frameAncestors: "frame-ancestors",
|
|
39
|
+
mediaSrc: "media-src",
|
|
40
|
+
objectSrc: "object-src",
|
|
41
|
+
workerSrc: "worker-src",
|
|
42
|
+
childSrc: "child-src",
|
|
43
|
+
baseUri: "base-uri",
|
|
44
|
+
formAction: "form-action",
|
|
45
|
+
manifestSrc: "manifest-src",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function csp(options: CspOptions): RouteMiddleware {
|
|
49
|
+
const usesNonce = detectsNonce(options)
|
|
50
|
+
const reportOnly = options.reportOnly ?? false
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
before: usesNonce
|
|
54
|
+
? ({ bind }: RequestContext) => {
|
|
55
|
+
const nonce = crypto.randomUUID()
|
|
56
|
+
bind({ cspNonce: nonce })
|
|
57
|
+
}
|
|
58
|
+
: undefined,
|
|
59
|
+
after: ({
|
|
60
|
+
response,
|
|
61
|
+
cspNonce,
|
|
62
|
+
}: ResponseContext & { cspNonce?: string }) => {
|
|
63
|
+
const value = buildPolicy(options, cspNonce)
|
|
64
|
+
const header = reportOnly
|
|
65
|
+
? HTTP.header.ContentSecurityPolicyReportOnly
|
|
66
|
+
: HTTP.header.ContentSecurityPolicy
|
|
67
|
+
response.headers.set(header, value)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectsNonce(options: CspOptions): boolean {
|
|
73
|
+
for (const key of Object.keys(directiveMap)) {
|
|
74
|
+
const sources = options[key as keyof CspOptions]
|
|
75
|
+
if (
|
|
76
|
+
Array.isArray(sources) &&
|
|
77
|
+
sources.includes(CSP.nonce as unknown as CspSource)
|
|
78
|
+
) {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildPolicy(options: CspOptions, nonce?: string): string {
|
|
86
|
+
const parts: string[] = []
|
|
87
|
+
|
|
88
|
+
for (const [key, directive] of Object.entries(directiveMap)) {
|
|
89
|
+
const sources = options[key as keyof CspOptions]
|
|
90
|
+
if (!Array.isArray(sources) || sources.length === 0) continue
|
|
91
|
+
|
|
92
|
+
const resolved = sources.map((source) => {
|
|
93
|
+
if (source === CSP.nonce) return `'nonce-${nonce}'`
|
|
94
|
+
return source as string
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
parts.push(`${directive} ${resolved.join(" ")}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (options.upgradeInsecureRequests) {
|
|
101
|
+
parts.push("upgrade-insecure-requests")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (options.reportTo) {
|
|
105
|
+
parts.push(`report-to ${options.reportTo}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return parts.join("; ")
|
|
109
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { HTTP } from "../HTTP"
|
|
2
|
+
import type { RequestContext, RouteMiddleware } from "../router.types"
|
|
3
|
+
import type { Cookies } from "./cookies"
|
|
4
|
+
|
|
5
|
+
export type CsrfOptions = {
|
|
6
|
+
readonly cookie?: string
|
|
7
|
+
readonly header?: string
|
|
8
|
+
readonly methods?: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function csrf(options?: CsrfOptions): RouteMiddleware {
|
|
12
|
+
const cookieName = options?.cookie ?? "csrf-token"
|
|
13
|
+
const headerName = options?.header ?? HTTP.header.XCsrfToken
|
|
14
|
+
const methods = options?.methods ?? [
|
|
15
|
+
HTTP.method.POST,
|
|
16
|
+
HTTP.method.PUT,
|
|
17
|
+
HTTP.method.PATCH,
|
|
18
|
+
HTTP.method.DELETE,
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
before: ({ request, cookies }: RequestContext & { cookies?: Cookies }) => {
|
|
23
|
+
if (!cookies) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"csrf() requires cookies() middleware to be in the stack",
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!methods.includes(request.method)) return
|
|
30
|
+
|
|
31
|
+
const origin = request.headers.get(HTTP.header.Origin)
|
|
32
|
+
const url = new URL(request.url)
|
|
33
|
+
if (origin && origin !== url.origin) {
|
|
34
|
+
return new Response(
|
|
35
|
+
JSON.stringify({ error: HTTP.statusText.Forbidden }),
|
|
36
|
+
{
|
|
37
|
+
status: HTTP.status.Forbidden,
|
|
38
|
+
statusText: HTTP.statusText.Forbidden,
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cookieToken = cookies.get(cookieName)
|
|
44
|
+
const headerToken = request.headers.get(headerName)
|
|
45
|
+
|
|
46
|
+
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
|
|
47
|
+
return new Response(
|
|
48
|
+
JSON.stringify({ error: HTTP.statusText.Forbidden }),
|
|
49
|
+
{
|
|
50
|
+
status: HTTP.status.Forbidden,
|
|
51
|
+
statusText: HTTP.statusText.Forbidden,
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
after: ({ cookies }: { cookies?: Cookies }) => {
|
|
57
|
+
if (!cookies) return
|
|
58
|
+
const token = crypto.randomUUID()
|
|
59
|
+
cookies.set(cookieName, token, {
|
|
60
|
+
httpOnly: false,
|
|
61
|
+
sameSite: "strict",
|
|
62
|
+
path: "/",
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RequestContext, RequestHandler } from "../router.types"
|
|
2
|
+
|
|
3
|
+
export function filter(
|
|
4
|
+
predicate: (context: RequestContext) => boolean | Promise<boolean>,
|
|
5
|
+
): (handler: RequestHandler) => RequestHandler {
|
|
6
|
+
return (handler) => async (context: RequestContext) => {
|
|
7
|
+
if (!(await predicate(context))) return
|
|
8
|
+
return context.dispatch(handler, [])
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HTTP } from "../HTTP"
|
|
2
|
+
import type { ResponseContext, RouteMiddleware } from "../router.types"
|
|
3
|
+
|
|
4
|
+
function make(value: string): RouteMiddleware {
|
|
5
|
+
return {
|
|
6
|
+
after: ({ response }: ResponseContext) => {
|
|
7
|
+
response.headers.set(HTTP.header.XFrameOptions, value)
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export namespace frameGuard {
|
|
13
|
+
export const deny = make(HTTP.frameOption.deny)
|
|
14
|
+
export const sameOrigin = make(HTTP.frameOption.sameOrigin)
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ResponseContext, RouteMiddleware } from "../router.types"
|
|
2
|
+
import { frameGuard } from "./frameGuard"
|
|
3
|
+
import { hsts, type HstsOptions } from "./hsts"
|
|
4
|
+
import { noSniff } from "./noSniff"
|
|
5
|
+
import { referrerPolicy } from "./referrerPolicy"
|
|
6
|
+
|
|
7
|
+
export type HardenedOptions = {
|
|
8
|
+
readonly noSniff?: false
|
|
9
|
+
readonly frameGuard?: false | "deny" | "sameOrigin"
|
|
10
|
+
readonly referrerPolicy?: false | keyof typeof referrerPolicy
|
|
11
|
+
readonly hsts?: false | HstsOptions
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hardened(options?: HardenedOptions): RouteMiddleware {
|
|
15
|
+
const parts: RouteMiddleware[] = []
|
|
16
|
+
|
|
17
|
+
if (options?.noSniff !== false) {
|
|
18
|
+
parts.push(noSniff)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (options?.frameGuard !== false) {
|
|
22
|
+
const value = options?.frameGuard ?? "deny"
|
|
23
|
+
parts.push(frameGuard[value])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (options?.referrerPolicy !== false) {
|
|
27
|
+
const value = options?.referrerPolicy ?? "strictOriginWhenCrossOrigin"
|
|
28
|
+
parts.push(referrerPolicy[value])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options?.hsts !== false) {
|
|
32
|
+
const hstsOptions = options?.hsts === undefined ? undefined : options.hsts
|
|
33
|
+
parts.push(hsts(hstsOptions))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
after: (context: ResponseContext) => {
|
|
38
|
+
for (const part of parts) {
|
|
39
|
+
if (part.after) part.after(context)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HTTP } from "../HTTP"
|
|
2
|
+
import type { ResponseContext, RouteMiddleware } from "../router.types"
|
|
3
|
+
|
|
4
|
+
export type HstsOptions = {
|
|
5
|
+
readonly maxAge?: number
|
|
6
|
+
readonly includeSubDomains?: boolean
|
|
7
|
+
readonly preload?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function hsts(options?: HstsOptions): RouteMiddleware {
|
|
11
|
+
const maxAge = options?.maxAge ?? 31536000
|
|
12
|
+
const includeSubDomains = options?.includeSubDomains ?? true
|
|
13
|
+
const preload = options?.preload ?? false
|
|
14
|
+
|
|
15
|
+
let value = `max-age=${maxAge}`
|
|
16
|
+
if (includeSubDomains) value += "; includeSubDomains"
|
|
17
|
+
if (preload) value += "; preload"
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
after: ({ response }: ResponseContext) => {
|
|
21
|
+
response.headers.set(HTTP.header.StrictTransportSecurity, value)
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RequestContext, RequestHandler } from "../router.types"
|
|
2
|
+
|
|
3
|
+
export function https(): RequestHandler {
|
|
4
|
+
return ({ url }: RequestContext) => {
|
|
5
|
+
if (url.protocol === "https:") return
|
|
6
|
+
url.protocol = "https:"
|
|
7
|
+
return Response.redirect(url, 301)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./bodyLimit"
|
|
2
|
+
export * from "./cache"
|
|
3
|
+
export * from "./cookies"
|
|
4
|
+
export * from "./cors"
|
|
5
|
+
export * from "./csp"
|
|
6
|
+
export * from "./csrf"
|
|
7
|
+
export * from "./filter"
|
|
8
|
+
export * from "./frameGuard"
|
|
9
|
+
export * from "./hardened"
|
|
10
|
+
export * from "./hsts"
|
|
11
|
+
export * from "./https"
|
|
12
|
+
export * from "./noSniff"
|
|
13
|
+
export * from "./rateLimit"
|
|
14
|
+
export * from "./referrerPolicy"
|
|
15
|
+
export * from "./requestId"
|
|
16
|
+
export * from "./setHeader"
|
|
17
|
+
export * from "./www"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { HTTP } from "../HTTP"
|
|
2
|
+
import type { ResponseContext, RouteMiddleware } from "../router.types"
|
|
3
|
+
|
|
4
|
+
export const noSniff: RouteMiddleware = {
|
|
5
|
+
after: ({ response }: ResponseContext) => {
|
|
6
|
+
response.headers.set(
|
|
7
|
+
HTTP.header.XContentTypeOptions,
|
|
8
|
+
HTTP.contentTypeOptions.nosniff,
|
|
9
|
+
)
|
|
10
|
+
},
|
|
11
|
+
}
|