@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.
Files changed (45) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +7 -0
  3. package/package.json +47 -0
  4. package/src/Assets.ts +4 -0
  5. package/src/CSP.ts +18 -0
  6. package/src/HTTP.ts +83 -0
  7. package/src/Router.ts +100 -0
  8. package/src/RouterError.ts +31 -0
  9. package/src/bun/bun.ts +37 -0
  10. package/src/bun/index.ts +1 -0
  11. package/src/bun/tsconfig.json +8 -0
  12. package/src/cloudflare/cloudflare.ts +43 -0
  13. package/src/cloudflare/index.ts +1 -0
  14. package/src/cloudflare/tsconfig.json +8 -0
  15. package/src/env.d.ts +1 -0
  16. package/src/global.d.ts +1 -0
  17. package/src/handler/app.ts +27 -0
  18. package/src/handler/assets.ts +10 -0
  19. package/src/handler/index.ts +7 -0
  20. package/src/handler/mount.ts +13 -0
  21. package/src/handler/noop.ts +3 -0
  22. package/src/handler/pattern.ts +71 -0
  23. package/src/handler/prefix.ts +32 -0
  24. package/src/handler/use.ts +36 -0
  25. package/src/index.ts +9 -0
  26. package/src/middleware/bodyLimit.ts +43 -0
  27. package/src/middleware/cache.ts +53 -0
  28. package/src/middleware/cookies.ts +76 -0
  29. package/src/middleware/cors.ts +113 -0
  30. package/src/middleware/csp.ts +109 -0
  31. package/src/middleware/csrf.ts +66 -0
  32. package/src/middleware/filter.ts +10 -0
  33. package/src/middleware/frameGuard.ts +15 -0
  34. package/src/middleware/hardened.ts +43 -0
  35. package/src/middleware/hsts.ts +24 -0
  36. package/src/middleware/https.ts +9 -0
  37. package/src/middleware/index.ts +17 -0
  38. package/src/middleware/noSniff.ts +11 -0
  39. package/src/middleware/rateLimit.ts +107 -0
  40. package/src/middleware/referrerPolicy.ts +27 -0
  41. package/src/middleware/requestId.ts +31 -0
  42. package/src/middleware/setHeader.ts +7 -0
  43. package/src/middleware/www.ts +12 -0
  44. package/src/route.ts +21 -0
  45. 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
+ }