@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,107 @@
|
|
|
1
|
+
import { HTTP } from "../HTTP";
|
|
2
|
+
import type {
|
|
3
|
+
RequestContext,
|
|
4
|
+
ResponseContext,
|
|
5
|
+
RouteMiddleware,
|
|
6
|
+
} from "../router.types";
|
|
7
|
+
|
|
8
|
+
export type RateLimitStore = {
|
|
9
|
+
increment(
|
|
10
|
+
key: string,
|
|
11
|
+
window: number,
|
|
12
|
+
): Promise<{ count: number; reset: number }>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type RateLimitOptions = {
|
|
16
|
+
readonly window?: number
|
|
17
|
+
readonly max?: number
|
|
18
|
+
readonly key?: (context: RequestContext) => string
|
|
19
|
+
readonly store?: RateLimitStore
|
|
20
|
+
readonly headers?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function rateLimit(options?: RateLimitOptions): RouteMiddleware {
|
|
24
|
+
const window = options?.window ?? 60
|
|
25
|
+
const max = options?.max ?? 100
|
|
26
|
+
const key = options?.key ?? rateLimit.ip
|
|
27
|
+
const store = options?.store ?? rateLimit.memory()
|
|
28
|
+
const headers = options?.headers ?? true
|
|
29
|
+
|
|
30
|
+
let lastResult: { count: number; reset: number } | undefined
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
before: async (context: RequestContext) => {
|
|
34
|
+
const id = key(context)
|
|
35
|
+
const result = await store.increment(id, window)
|
|
36
|
+
lastResult = result
|
|
37
|
+
|
|
38
|
+
if (result.count > max) {
|
|
39
|
+
const response = new Response(
|
|
40
|
+
JSON.stringify({ error: HTTP.statusText.TooManyRequests }),
|
|
41
|
+
{
|
|
42
|
+
status: HTTP.status.TooManyRequests,
|
|
43
|
+
statusText: HTTP.statusText.TooManyRequests,
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
if (headers) {
|
|
47
|
+
response.headers.set(HTTP.header.XRateLimitLimit, String(max))
|
|
48
|
+
response.headers.set(HTTP.header.XRateLimitRemaining, "0")
|
|
49
|
+
response.headers.set(
|
|
50
|
+
HTTP.header.XRateLimitReset,
|
|
51
|
+
String(result.reset),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
return response
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
after: ({ response }: ResponseContext) => {
|
|
58
|
+
if (headers && lastResult) {
|
|
59
|
+
const remaining = Math.max(0, max - lastResult.count)
|
|
60
|
+
response.headers.set(HTTP.header.XRateLimitLimit, String(max))
|
|
61
|
+
response.headers.set(HTTP.header.XRateLimitRemaining, String(remaining))
|
|
62
|
+
response.headers.set(
|
|
63
|
+
HTTP.header.XRateLimitReset,
|
|
64
|
+
String(lastResult.reset),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export namespace rateLimit {
|
|
72
|
+
export function ip({ request }: RequestContext): string {
|
|
73
|
+
return (
|
|
74
|
+
request.headers.get(HTTP.header.CFConnectingIP) ??
|
|
75
|
+
request.headers.get(HTTP.header.XForwardedFor)?.split(",")[0]?.trim() ??
|
|
76
|
+
"unknown"
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function memory(): RateLimitStore {
|
|
81
|
+
const windows = new Map<string, { count: number; reset: number }>()
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
async increment(key: string, window: number) {
|
|
85
|
+
const now = Math.floor(Date.now() / 1000)
|
|
86
|
+
const windowStart = now - (now % window)
|
|
87
|
+
const windowKey = `${key}:${windowStart}`
|
|
88
|
+
const reset = windowStart + window
|
|
89
|
+
|
|
90
|
+
const entry = windows.get(windowKey)
|
|
91
|
+
if (entry) {
|
|
92
|
+
entry.count++
|
|
93
|
+
return { count: entry.count, reset }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
windows.set(windowKey, { count: 1, reset })
|
|
97
|
+
|
|
98
|
+
// Clean up expired windows
|
|
99
|
+
for (const [k, v] of windows) {
|
|
100
|
+
if (v.reset < now) windows.delete(k)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { count: 1, reset }
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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.ReferrerPolicy, value)
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export namespace referrerPolicy {
|
|
13
|
+
export const noReferrer = make(HTTP.referrerPolicy.noReferrer)
|
|
14
|
+
export const noReferrerWhenDowngrade = make(
|
|
15
|
+
HTTP.referrerPolicy.noReferrerWhenDowngrade,
|
|
16
|
+
)
|
|
17
|
+
export const origin = make(HTTP.referrerPolicy.origin)
|
|
18
|
+
export const originWhenCrossOrigin = make(
|
|
19
|
+
HTTP.referrerPolicy.originWhenCrossOrigin,
|
|
20
|
+
)
|
|
21
|
+
export const sameOrigin = make(HTTP.referrerPolicy.sameOrigin)
|
|
22
|
+
export const strictOrigin = make(HTTP.referrerPolicy.strictOrigin)
|
|
23
|
+
export const strictOriginWhenCrossOrigin = make(
|
|
24
|
+
HTTP.referrerPolicy.strictOriginWhenCrossOrigin,
|
|
25
|
+
)
|
|
26
|
+
export const unsafeUrl = make(HTTP.referrerPolicy.unsafeUrl)
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { HTTP } from "../HTTP"
|
|
2
|
+
import type {
|
|
3
|
+
RequestContext,
|
|
4
|
+
ResponseContext,
|
|
5
|
+
RouteMiddleware,
|
|
6
|
+
} from "../router.types"
|
|
7
|
+
|
|
8
|
+
export type RequestIdOptions = {
|
|
9
|
+
readonly header?: string
|
|
10
|
+
readonly generate?: () => string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function requestId(options?: RequestIdOptions): RouteMiddleware {
|
|
14
|
+
const header = options?.header ?? HTTP.header.XRequestId
|
|
15
|
+
const generate = options?.generate ?? (() => crypto.randomUUID())
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
before: ({ request, bind }: RequestContext) => {
|
|
19
|
+
const id = request.headers.get(header) ?? generate()
|
|
20
|
+
bind({ requestId: id })
|
|
21
|
+
},
|
|
22
|
+
after: ({
|
|
23
|
+
response,
|
|
24
|
+
requestId,
|
|
25
|
+
}: ResponseContext & { requestId: string }) => {
|
|
26
|
+
if (requestId) {
|
|
27
|
+
response.headers.set(header, requestId)
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RequestContext, RequestHandler } from "../router.types"
|
|
2
|
+
|
|
3
|
+
export function www({ secure }: { readonly secure?: boolean }): RequestHandler {
|
|
4
|
+
return ({ url }: RequestContext) => {
|
|
5
|
+
const isWww = url.hostname.startsWith("www.")
|
|
6
|
+
const isSecure = url.protocol === "https:"
|
|
7
|
+
if (isWww && (secure ? isSecure : true)) return
|
|
8
|
+
url.hostname = `www.${url.hostname}`
|
|
9
|
+
if (secure) url.protocol = "https:"
|
|
10
|
+
return Response.redirect(url, 301)
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/route.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Router } from "./Router"
|
|
2
|
+
import type { RequestHandler, RouterFetch, RouterOptions } from "./router.types"
|
|
3
|
+
|
|
4
|
+
export type MaybeHandler = RequestHandler | null | undefined | false | 0
|
|
5
|
+
|
|
6
|
+
export function route(...handlers: MaybeHandler[]): RouterFetch
|
|
7
|
+
export function route(
|
|
8
|
+
options: RouterOptions,
|
|
9
|
+
...handlers: MaybeHandler[]
|
|
10
|
+
): RouterFetch
|
|
11
|
+
export function route(
|
|
12
|
+
options: RouterOptions | MaybeHandler,
|
|
13
|
+
...handlers: MaybeHandler[]
|
|
14
|
+
) {
|
|
15
|
+
const actualHandlers = handlers.filter((h): h is RequestHandler => !!h)
|
|
16
|
+
const router =
|
|
17
|
+
typeof options === "function"
|
|
18
|
+
? new Router([options, ...actualHandlers])
|
|
19
|
+
: new Router(actualHandlers, options || undefined)
|
|
20
|
+
return router.route.bind(router)
|
|
21
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// oxlint-disable typescript/no-explicit-any
|
|
2
|
+
import type { Container } from "@sigitex/bind"
|
|
3
|
+
|
|
4
|
+
export type RouteTree = {
|
|
5
|
+
[key: string]: string | RouteTree
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type RouterFetch = (
|
|
9
|
+
request: Request,
|
|
10
|
+
env: Env,
|
|
11
|
+
) => Promise<Response>
|
|
12
|
+
|
|
13
|
+
export type RequestContext = {
|
|
14
|
+
readonly request: Request
|
|
15
|
+
readonly env: Env
|
|
16
|
+
readonly url: URL
|
|
17
|
+
readonly bind: RouterBind
|
|
18
|
+
readonly dispatch: RouterDispatch
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type RouterBind = (bindings: { [key: string]: any }) => void
|
|
22
|
+
|
|
23
|
+
export type RouterDispatch = (
|
|
24
|
+
handler: RequestHandler,
|
|
25
|
+
middlewares: RouteMiddleware[],
|
|
26
|
+
) => Promise<Response | undefined>
|
|
27
|
+
|
|
28
|
+
export type ResponseContext = RequestContext & {
|
|
29
|
+
readonly response: Response
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type RequestHandler = (
|
|
33
|
+
context: any,
|
|
34
|
+
) => unknown
|
|
35
|
+
|
|
36
|
+
export type ResponseHandler = (
|
|
37
|
+
context: any,
|
|
38
|
+
) => unknown
|
|
39
|
+
|
|
40
|
+
export type RouterOptions = {
|
|
41
|
+
readonly container?: Container
|
|
42
|
+
readonly middlewares?: RouteMiddleware[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type RouteMiddleware = {
|
|
46
|
+
readonly before?: RequestHandler
|
|
47
|
+
readonly after?: ResponseHandler
|
|
48
|
+
}
|