@mpen/routekit 0.1.0 → 0.1.1
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/dist/bin.d.mts +4 -0
- package/dist/client/react.d.mts +178 -0
- package/dist/client/react.mjs +142 -0
- package/dist/client.d.mts +433 -0
- package/dist/client.mjs +264 -0
- package/dist/content-BuDOmhH_.mjs +102 -0
- package/dist/core-CzUCxvGk.d.mts +140 -0
- package/dist/core-DbmQauwS.mjs +81 -0
- package/dist/handlers.d.mts +72 -0
- package/dist/handlers.mjs +153 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +1152 -0
- package/dist/middleware.d.mts +388 -0
- package/dist/middleware.mjs +1222 -0
- package/dist/request-Dn0zc-xm.mjs +1025 -0
- package/dist/response/content.d.mts +79 -0
- package/dist/response/content.mjs +2 -0
- package/dist/response/json-rpc.d.mts +1 -0
- package/dist/response/json-rpc.mjs +1 -0
- package/dist/response/problem/valibot.d.mts +230 -0
- package/dist/response/problem/valibot.mjs +258 -0
- package/dist/response/problem.d.mts +415 -0
- package/dist/response/problem.mjs +183 -0
- package/dist/response/status.d.mts +45 -0
- package/dist/response/status.mjs +2 -0
- package/dist/responses-B379Ep9Y.d.mts +296 -0
- package/dist/responses-BpVrgeYi.mjs +101 -0
- package/dist/router-Cwb7ak0J.d.mts +1819 -0
- package/dist/routes.d.mts +282 -0
- package/dist/routes.mjs +311 -0
- package/dist/status-C-8mw-FB.mjs +59 -0
- package/dist/valibot-D7liFYyB.d.mts +290 -0
- package/dist/valibot-Du97X-TS.mjs +326 -0
- package/package.json +8 -2
- package/src/bin/gen-api-client.test.ts +0 -70
- package/src/bin/gen-api-client.ts +0 -986
- package/src/client/headers.ts +0 -31
- package/src/client/index.ts +0 -8
- package/src/client/promise.ts +0 -11
- package/src/client/react/index.test.tsx +0 -266
- package/src/client/react/index.ts +0 -431
- package/src/client/responses.test.ts +0 -151
- package/src/client/responses.ts +0 -278
- package/src/client/transport.ts +0 -74
- package/src/client/transports/body-codec.ts +0 -61
- package/src/client/transports/fetch.ts +0 -113
- package/src/client/tsconfig.json +0 -9
- package/src/client/types.ts +0 -15
- package/src/client/url.ts +0 -31
- package/src/index.ts +0 -63
- package/src/router/fetch-types.ts +0 -13
- package/src/router/handlers/index.ts +0 -2
- package/src/router/handlers/openapi/index.ts +0 -2
- package/src/router/handlers/openapi/openapi.ts +0 -293
- package/src/router/integration/zod-openapi.test.ts +0 -74
- package/src/router/lib/charset.test.ts +0 -22
- package/src/router/lib/charset.ts +0 -133
- package/src/router/lib/collections.ts +0 -3
- package/src/router/lib/format.test.ts +0 -67
- package/src/router/lib/format.ts +0 -35
- package/src/router/lib/host.ts +0 -4
- package/src/router/lib/json-schema.ts +0 -6
- package/src/router/lib/media-type.test.ts +0 -122
- package/src/router/lib/media-type.ts +0 -289
- package/src/router/lib/pathname.test.ts +0 -18
- package/src/router/lib/pathname.ts +0 -19
- package/src/router/lib/route-names.ts +0 -70
- package/src/router/lib/route-normalize.test.ts +0 -36
- package/src/router/lib/route-normalize.ts +0 -67
- package/src/router/lib/schema-merge.ts +0 -56
- package/src/router/middleware/accept-ctx.test.ts +0 -33
- package/src/router/middleware/accept-ctx.ts +0 -12
- package/src/router/middleware/body-limit.test.ts +0 -112
- package/src/router/middleware/body-limit.ts +0 -121
- package/src/router/middleware/content-type-context.ts +0 -0
- package/src/router/middleware/cors.test.ts +0 -269
- package/src/router/middleware/cors.ts +0 -490
- package/src/router/middleware/csrf.test.ts +0 -106
- package/src/router/middleware/csrf.ts +0 -192
- package/src/router/middleware/define.ts +0 -249
- package/src/router/middleware/index.ts +0 -34
- package/src/router/middleware/jsxhtml-response.ts +0 -0
- package/src/router/middleware/oas-swagger.ts +0 -0
- package/src/router/middleware/rate-limit.test.ts +0 -886
- package/src/router/middleware/rate-limit.ts +0 -920
- package/src/router/middleware/request-id-ctx.test.ts +0 -183
- package/src/router/middleware/request-id-ctx.ts +0 -135
- package/src/router/middleware/request-logger-format.test.ts +0 -16
- package/src/router/middleware/request-logger-format.ts +0 -269
- package/src/router/middleware/request-logger.test.ts +0 -267
- package/src/router/middleware/request-logger.ts +0 -131
- package/src/router/middleware/start-time-ctx.ts +0 -5
- package/src/router/request.ts +0 -611
- package/src/router/response/core.ts +0 -181
- package/src/router/response/directives.ts +0 -233
- package/src/router/response/formats/content/bodyless.ts +0 -54
- package/src/router/response/formats/content/content.ts +0 -79
- package/src/router/response/formats/content/index.ts +0 -2
- package/src/router/response/formats/json-rpc/index.ts +0 -2
- package/src/router/response/formats/problem/badRequest.ts +0 -90
- package/src/router/response/formats/problem/conflict.ts +0 -90
- package/src/router/response/formats/problem/created.ts +0 -40
- package/src/router/response/formats/problem/index.ts +0 -27
- package/src/router/response/formats/problem/notFound.ts +0 -90
- package/src/router/response/formats/problem/permissionDenied.ts +0 -90
- package/src/router/response/formats/problem/problem.test.ts +0 -888
- package/src/router/response/formats/problem/rateLimited.ts +0 -90
- package/src/router/response/formats/problem/responses.ts +0 -219
- package/src/router/response/formats/problem/root-errors.ts +0 -48
- package/src/router/response/formats/problem/sessionExpired.ts +0 -90
- package/src/router/response/formats/problem/types.ts +0 -170
- package/src/router/response/formats/problem/unauthenticated.ts +0 -90
- package/src/router/response/formats/problem/valibot.ts +0 -410
- package/src/router/response/formats/status/index.ts +0 -1
- package/src/router/response/formats/status/responses.ts +0 -59
- package/src/router/response/formats/status/status.test.ts +0 -21
- package/src/router/response/framers.ts +0 -85
- package/src/router/response/index.ts +0 -28
- package/src/router/response/openapi.test.ts +0 -96
- package/src/router/response/openapi.ts +0 -1
- package/src/router/response/serializers.ts +0 -66
- package/src/router/response/stream.ts +0 -35
- package/src/router/router.test.ts +0 -1571
- package/src/router/router.ts +0 -1965
- package/src/router/routes/index.ts +0 -46
- package/src/router/routes/valibot/index.ts +0 -18
- package/src/router/routes/valibot/valibot.ts +0 -1393
- package/src/router/routes/valibot.test.ts +0 -286
- package/src/router/routes/zod/index.ts +0 -18
- package/src/router/routes/zod/zod.ts +0 -1318
- package/src/router/routes/zod.test.ts +0 -280
- package/src/router/server-interface.ts +0 -31
- package/src/router/types.ts +0 -657
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun test
|
|
2
|
-
import { describe, expect, it } from 'bun:test'
|
|
3
|
-
import { HttpMethod } from '@mpen/http'
|
|
4
|
-
import { Router } from '../router'
|
|
5
|
-
import { acceptCtx } from './accept-ctx'
|
|
6
|
-
|
|
7
|
-
describe(acceptCtx.name, () => {
|
|
8
|
-
it('adds parsed Accept header values to the context', async () => {
|
|
9
|
-
const router = new Router()
|
|
10
|
-
|
|
11
|
-
router.use(acceptCtx())
|
|
12
|
-
router.add({
|
|
13
|
-
method: HttpMethod.GET,
|
|
14
|
-
path: '/',
|
|
15
|
-
handler: ({ accept }) => new Response(JSON.stringify(accept)),
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const request = new Request('https://example.com/', {
|
|
19
|
-
headers: {
|
|
20
|
-
accept: 'text/plain;q=0.5, application/json, text/html;q=0.9,application/yaml;q=1',
|
|
21
|
-
},
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
const response = await router.fetch(request)
|
|
25
|
-
|
|
26
|
-
expect(await response.json()).toEqual([
|
|
27
|
-
{ type: 'application/json', q: 1 },
|
|
28
|
-
{ type: 'application/yaml', q: 1 },
|
|
29
|
-
{ type: 'text/html', q: 0.9 },
|
|
30
|
-
{ type: 'text/plain', q: 0.5 },
|
|
31
|
-
])
|
|
32
|
-
})
|
|
33
|
-
})
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { AcceptMediaRange, ContextMiddleware } from '../types'
|
|
2
|
-
import { parseAcceptHeader } from '../lib/media-type'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Attach parsed Accept header values to the request context.
|
|
6
|
-
*
|
|
7
|
-
* @returns Middleware that adds `accept` to the request context.
|
|
8
|
-
*/
|
|
9
|
-
export const acceptCtx = (): ContextMiddleware<{ accept: AcceptMediaRange[] }> => (ctx) => {
|
|
10
|
-
const header = ctx.request.headers.get('accept')
|
|
11
|
-
ctx.accept = parseAcceptHeader(header ?? '*/*')
|
|
12
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun test
|
|
2
|
-
import { describe, expect, it, mock } from 'bun:test'
|
|
3
|
-
import { HttpMethod, HttpStatus } from '@mpen/http'
|
|
4
|
-
import { Router } from '../router'
|
|
5
|
-
import { bodyLimit } from './body-limit'
|
|
6
|
-
|
|
7
|
-
const encoder = new TextEncoder()
|
|
8
|
-
|
|
9
|
-
function makeStream(chunks: string[]): ReadableStream<Uint8Array> {
|
|
10
|
-
return new ReadableStream<Uint8Array>({
|
|
11
|
-
start(controller) {
|
|
12
|
-
for (const chunk of chunks) {
|
|
13
|
-
controller.enqueue(encoder.encode(chunk))
|
|
14
|
-
}
|
|
15
|
-
controller.close()
|
|
16
|
-
},
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe(bodyLimit.name, () => {
|
|
21
|
-
it('rejects immediately when Content-Length exceeds maxSize', async () => {
|
|
22
|
-
const router = new Router()
|
|
23
|
-
const handler = mock(() => new Response('ok'))
|
|
24
|
-
|
|
25
|
-
router.use(bodyLimit({ maxSize: 9 }))
|
|
26
|
-
router.add({ method: HttpMethod.POST, path: '/upload', handler })
|
|
27
|
-
|
|
28
|
-
const request = new Request('https://example.com/upload', {
|
|
29
|
-
method: HttpMethod.POST,
|
|
30
|
-
headers: { 'content-length': '10' },
|
|
31
|
-
body: makeStream(['ok']),
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const response = await router.fetch(request)
|
|
35
|
-
|
|
36
|
-
expect(response.status).toBe(HttpStatus.PAYLOAD_TOO_LARGE)
|
|
37
|
-
expect(handler).not.toHaveBeenCalled()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('rejects when the streamed body exceeds maxSize', async () => {
|
|
41
|
-
const router = new Router()
|
|
42
|
-
|
|
43
|
-
router.use(bodyLimit({ maxSize: 4 }))
|
|
44
|
-
router.add({
|
|
45
|
-
method: HttpMethod.POST,
|
|
46
|
-
path: '/upload',
|
|
47
|
-
handler: async ({ request }) => new Response(await request.body.text()),
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const request = new Request('https://example.com/upload', {
|
|
51
|
-
method: HttpMethod.POST,
|
|
52
|
-
body: makeStream(['1234', '5']),
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const response = await router.fetch(request)
|
|
56
|
-
|
|
57
|
-
expect(response.status).toBe(HttpStatus.PAYLOAD_TOO_LARGE)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('rejects when Content-Length does not match the received bytes', async () => {
|
|
61
|
-
const router = new Router()
|
|
62
|
-
|
|
63
|
-
router.use(bodyLimit({ maxSize: 10 }))
|
|
64
|
-
router.add({
|
|
65
|
-
method: HttpMethod.POST,
|
|
66
|
-
path: '/upload',
|
|
67
|
-
handler: async ({ request }) => new Response(await request.body.text()),
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const request = new Request('https://example.com/upload', {
|
|
71
|
-
method: HttpMethod.POST,
|
|
72
|
-
headers: { 'content-length': '4' },
|
|
73
|
-
body: makeStream(['abc']),
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const response = await router.fetch(request)
|
|
77
|
-
|
|
78
|
-
expect(response.status).toBe(HttpStatus.BAD_REQUEST)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('allows requests within the limit and matching Content-Length', async () => {
|
|
82
|
-
const router = new Router()
|
|
83
|
-
|
|
84
|
-
router.use(bodyLimit({ maxSize: 10 }))
|
|
85
|
-
router.add({
|
|
86
|
-
method: HttpMethod.POST,
|
|
87
|
-
path: '/upload',
|
|
88
|
-
handler: async ({ request }) => new Response(await request.body.text()),
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const request = new Request('https://example.com/upload', {
|
|
92
|
-
method: HttpMethod.POST,
|
|
93
|
-
headers: { 'content-length': '5' },
|
|
94
|
-
body: makeStream(['hello']),
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
const response = await router.fetch(request)
|
|
98
|
-
|
|
99
|
-
expect(response.status).toBe(HttpStatus.OK)
|
|
100
|
-
expect(await response.text()).toBe('hello')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('declares its rejection responses on affected routes', () => {
|
|
104
|
-
const router = new Router().use(bodyLimit({ maxSize: 10 }))
|
|
105
|
-
router.add({ method: HttpMethod.POST, path: '/upload', handler: () => new Response() })
|
|
106
|
-
|
|
107
|
-
expect(router.getRoutes()[0]?.schema?.response?.body).toEqual({
|
|
108
|
-
[HttpStatus.BAD_REQUEST]: { type: 'string' },
|
|
109
|
-
[HttpStatus.PAYLOAD_TOO_LARGE]: { type: 'string' },
|
|
110
|
-
})
|
|
111
|
-
})
|
|
112
|
-
})
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { HttpStatus } from '@mpen/http'
|
|
2
|
-
import { RequestBodyLengthMismatchError, RequestBodyTooLargeError } from '../request'
|
|
3
|
-
import { text } from '../response/formats/content'
|
|
4
|
-
import { defineMiddleware, type DeclaredMiddleware } from './define'
|
|
5
|
-
import type { AnyContext } from '../types'
|
|
6
|
-
|
|
7
|
-
export interface MaxContentSizeOptions {
|
|
8
|
-
maxSize: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const utf8encoder = new TextEncoder()
|
|
12
|
-
|
|
13
|
-
function parseContentLength(value: string | null): number | null {
|
|
14
|
-
if (!value) return null
|
|
15
|
-
const trimmed = value.trim()
|
|
16
|
-
if (!trimmed) return null
|
|
17
|
-
const parsed = Number(trimmed)
|
|
18
|
-
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) return null
|
|
19
|
-
return parsed
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function chunkByteLength(chunk: Uint8Array | string): number {
|
|
23
|
-
if (typeof chunk === 'string') return utf8encoder.encode(chunk).length
|
|
24
|
-
return chunk.byteLength
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Enforce a maximum request body size while preserving access to the incoming stream.
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```ts
|
|
32
|
-
* router.use(bodyLimit({maxSize: 1024 * 1024}))
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* @param options - Configuration for maximum request body size enforcement.
|
|
36
|
-
* @returns Middleware that rejects oversized bodies and mismatched Content-Length values.
|
|
37
|
-
*/
|
|
38
|
-
export function bodyLimit<Ctx extends object = AnyContext>(
|
|
39
|
-
options: MaxContentSizeOptions,
|
|
40
|
-
): DeclaredMiddleware<{}, Ctx> {
|
|
41
|
-
const textResponse = {
|
|
42
|
-
schema: { type: 'string' },
|
|
43
|
-
parse(value: unknown): string {
|
|
44
|
-
if (typeof value !== 'string') {
|
|
45
|
-
throw new TypeError('Body limit responses must contain a string body.')
|
|
46
|
-
}
|
|
47
|
-
return value
|
|
48
|
-
},
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return defineMiddleware({
|
|
52
|
-
responses: {
|
|
53
|
-
[HttpStatus.BAD_REQUEST]: textResponse,
|
|
54
|
-
[HttpStatus.PAYLOAD_TOO_LARGE]: textResponse,
|
|
55
|
-
},
|
|
56
|
-
async run(ctx, { next, forward, respond }) {
|
|
57
|
-
const maxSize = options.maxSize
|
|
58
|
-
const contentLength = parseContentLength(ctx.request.headers.get('content-length'))
|
|
59
|
-
|
|
60
|
-
if (contentLength != null && contentLength > maxSize) {
|
|
61
|
-
void ctx.request.body.stream()?.cancel()
|
|
62
|
-
return respond(text('Payload Too Large', { status: HttpStatus.PAYLOAD_TOO_LARGE }))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const bodyStream = ctx.request.body.stream()
|
|
66
|
-
if (!bodyStream) {
|
|
67
|
-
if (contentLength != null && contentLength !== 0) {
|
|
68
|
-
return respond(text('Bad Request', { status: HttpStatus.BAD_REQUEST }))
|
|
69
|
-
}
|
|
70
|
-
return forward(await next())
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const reader = bodyStream.getReader()
|
|
74
|
-
let bytesRead = 0
|
|
75
|
-
|
|
76
|
-
const monitoredBody = new ReadableStream<Uint8Array>({
|
|
77
|
-
async pull(controller) {
|
|
78
|
-
const result = await reader.read()
|
|
79
|
-
if (result.done) {
|
|
80
|
-
if (contentLength != null && bytesRead !== contentLength) {
|
|
81
|
-
controller.error(
|
|
82
|
-
new RequestBodyLengthMismatchError(contentLength, bytesRead),
|
|
83
|
-
)
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
controller.close()
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const value = result.value
|
|
91
|
-
bytesRead += chunkByteLength(value)
|
|
92
|
-
if (bytesRead > maxSize) {
|
|
93
|
-
await reader.cancel()
|
|
94
|
-
controller.error(new RequestBodyTooLargeError(maxSize, bytesRead))
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
controller.enqueue(value)
|
|
98
|
-
},
|
|
99
|
-
cancel(reason) {
|
|
100
|
-
return reader.cancel(reason)
|
|
101
|
-
},
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
ctx.request = ctx.request.withBody(ctx.request.body.withStream(monitoredBody))
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
return forward(await next())
|
|
108
|
-
} catch (err) {
|
|
109
|
-
if (err instanceof RequestBodyTooLargeError) {
|
|
110
|
-
return respond(
|
|
111
|
-
text('Payload Too Large', { status: HttpStatus.PAYLOAD_TOO_LARGE }),
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
if (err instanceof RequestBodyLengthMismatchError) {
|
|
115
|
-
return respond(text('Bad Request', { status: HttpStatus.BAD_REQUEST }))
|
|
116
|
-
}
|
|
117
|
-
throw err
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
})
|
|
121
|
-
}
|
|
File without changes
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun test
|
|
2
|
-
import { describe, expect, it, mock } from 'bun:test'
|
|
3
|
-
import { HttpMethod, HttpStatus } from '@mpen/http'
|
|
4
|
-
import { Router } from '../router'
|
|
5
|
-
import { cors } from './cors'
|
|
6
|
-
|
|
7
|
-
describe(cors.name, () => {
|
|
8
|
-
it('adds wildcard allow-origin by default', async () => {
|
|
9
|
-
const router = new Router()
|
|
10
|
-
router.use(cors({ origin: '*' }))
|
|
11
|
-
router.add({
|
|
12
|
-
method: HttpMethod.GET,
|
|
13
|
-
path: '/data',
|
|
14
|
-
handler: () => new Response('ok'),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const response = await router.fetch(
|
|
18
|
-
new Request('https://api.example.com/data', {
|
|
19
|
-
headers: { origin: 'https://app.example.com' },
|
|
20
|
-
}),
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
expect(response.headers.get('access-control-allow-origin')).toBe('*')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('adds CORS headers to logical response bodies', async () => {
|
|
27
|
-
const router = new Router()
|
|
28
|
-
router.use(cors({ origin: '*' }))
|
|
29
|
-
router.add({
|
|
30
|
-
method: HttpMethod.GET,
|
|
31
|
-
path: '/data',
|
|
32
|
-
handler: () => ({ ok: true }),
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
const response = await router.fetch(
|
|
36
|
-
new Request('https://api.example.com/data', {
|
|
37
|
-
headers: { origin: 'https://app.example.com' },
|
|
38
|
-
}),
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
expect(response.headers.get('access-control-allow-origin')).toBe('*')
|
|
42
|
-
expect(await response.json()).toEqual({ ok: true })
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('supports an origin resolver function', async () => {
|
|
46
|
-
const router = new Router()
|
|
47
|
-
router.use(
|
|
48
|
-
cors({
|
|
49
|
-
origin: (origin) => (origin?.endsWith('.example.com') ? origin : null),
|
|
50
|
-
}),
|
|
51
|
-
)
|
|
52
|
-
router.add({
|
|
53
|
-
method: HttpMethod.GET,
|
|
54
|
-
path: '/data',
|
|
55
|
-
handler: () => new Response('ok'),
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const allowed = await router.fetch(
|
|
59
|
-
new Request('https://api.example.com/data', {
|
|
60
|
-
headers: { origin: 'https://app.example.com' },
|
|
61
|
-
}),
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
const denied = await router.fetch(
|
|
65
|
-
new Request('https://api.example.com/data', {
|
|
66
|
-
headers: { origin: 'https://evil.example' },
|
|
67
|
-
}),
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
expect(allowed.headers.get('access-control-allow-origin')).toBe('https://app.example.com')
|
|
71
|
-
expect(denied.headers.has('access-control-allow-origin')).toBe(false)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('echoes the origin when credentials are enabled', async () => {
|
|
75
|
-
const router = new Router()
|
|
76
|
-
router.use(cors({ origin: '*', credentials: true }))
|
|
77
|
-
router.add({
|
|
78
|
-
method: HttpMethod.GET,
|
|
79
|
-
path: '/data',
|
|
80
|
-
handler: () => new Response('ok'),
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
const response = await router.fetch(
|
|
84
|
-
new Request('https://api.example.com/data', {
|
|
85
|
-
headers: { origin: 'https://app.example.com' },
|
|
86
|
-
}),
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
expect(response.headers.get('access-control-allow-origin')).toBe('https://app.example.com')
|
|
90
|
-
expect(response.headers.get('access-control-allow-credentials')).toBe('true')
|
|
91
|
-
expect(response.headers.get('vary')).toContain('Origin')
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('exposes response headers when configured', async () => {
|
|
95
|
-
const router = new Router()
|
|
96
|
-
router.use(cors({ origin: '*', exposeHeaders: ['x-trace', 'x-request-id'] }))
|
|
97
|
-
router.add({
|
|
98
|
-
method: HttpMethod.GET,
|
|
99
|
-
path: '/data',
|
|
100
|
-
handler: () => new Response('ok'),
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
const response = await router.fetch(
|
|
104
|
-
new Request('https://api.example.com/data', {
|
|
105
|
-
headers: { origin: 'https://app.example.com' },
|
|
106
|
-
}),
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
expect(response.headers.get('access-control-expose-headers')).toBe('x-trace, x-request-id')
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('accepts a static allowMethods list', async () => {
|
|
113
|
-
const router = new Router()
|
|
114
|
-
router.use(cors({ origin: '*', allowMethods: ['GET', 'POST'] }))
|
|
115
|
-
router.add({
|
|
116
|
-
method: HttpMethod.OPTIONS,
|
|
117
|
-
path: '/widgets',
|
|
118
|
-
handler: () => new Response('ok'),
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
const response = await router.fetch(
|
|
122
|
-
new Request('https://api.example.com/widgets', {
|
|
123
|
-
method: HttpMethod.OPTIONS,
|
|
124
|
-
headers: {
|
|
125
|
-
origin: 'https://app.example.com',
|
|
126
|
-
'access-control-request-method': 'POST',
|
|
127
|
-
},
|
|
128
|
-
}),
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
expect(response.headers.get('access-control-allow-methods')).toBe('GET, POST')
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('accepts a dynamic allowMethods resolver', async () => {
|
|
135
|
-
const router = new Router()
|
|
136
|
-
router.use(
|
|
137
|
-
cors({
|
|
138
|
-
origin: '*',
|
|
139
|
-
allowMethods: (origin) =>
|
|
140
|
-
origin === 'https://app.example.com' ? ['GET'] : ['POST'],
|
|
141
|
-
}),
|
|
142
|
-
)
|
|
143
|
-
router.add({
|
|
144
|
-
method: HttpMethod.OPTIONS,
|
|
145
|
-
path: '/widgets',
|
|
146
|
-
handler: () => new Response('ok'),
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
const response = await router.fetch(
|
|
150
|
-
new Request('https://api.example.com/widgets', {
|
|
151
|
-
method: HttpMethod.OPTIONS,
|
|
152
|
-
headers: {
|
|
153
|
-
origin: 'https://app.example.com',
|
|
154
|
-
'access-control-request-method': 'POST',
|
|
155
|
-
},
|
|
156
|
-
}),
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
expect(response.headers.get('access-control-allow-methods')).toBe('GET')
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('allows localhost origins when dev is enabled', async () => {
|
|
163
|
-
const router = new Router()
|
|
164
|
-
router.use(cors({ origin: 'https://app.example.com', dev: true }))
|
|
165
|
-
router.add({
|
|
166
|
-
method: HttpMethod.GET,
|
|
167
|
-
path: '/data',
|
|
168
|
-
handler: () => new Response('ok'),
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
const response = await router.fetch(
|
|
172
|
-
new Request('https://api.example.com/data', {
|
|
173
|
-
headers: { origin: 'http://localhost:3000' },
|
|
174
|
-
}),
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
expect(response.headers.get('access-control-allow-origin')).toBe('http://localhost:3000')
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('allows localhost origins when allowLocalhost is enabled', async () => {
|
|
181
|
-
const router = new Router()
|
|
182
|
-
router.use(cors({ origin: 'https://app.example.com', allowLocalhost: true }))
|
|
183
|
-
router.add({
|
|
184
|
-
method: HttpMethod.GET,
|
|
185
|
-
path: '/data',
|
|
186
|
-
handler: () => new Response('ok'),
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const response = await router.fetch(
|
|
190
|
-
new Request('https://api.example.com/data', {
|
|
191
|
-
headers: { origin: 'http://127.0.0.1:3000' },
|
|
192
|
-
}),
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
expect(response.headers.get('access-control-allow-origin')).toBe('http://127.0.0.1:3000')
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('handles CORS preflight requests', async () => {
|
|
199
|
-
const router = new Router()
|
|
200
|
-
const handler = mock(() => new Response('ok'))
|
|
201
|
-
router.use(cors({ origin: '*', maxAge: 600 }))
|
|
202
|
-
router.add({
|
|
203
|
-
method: HttpMethod.OPTIONS,
|
|
204
|
-
path: '/widgets',
|
|
205
|
-
handler,
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
const response = await router.fetch(
|
|
209
|
-
new Request('https://api.example.com/widgets', {
|
|
210
|
-
method: HttpMethod.OPTIONS,
|
|
211
|
-
headers: {
|
|
212
|
-
origin: 'https://app.example.com',
|
|
213
|
-
'access-control-request-method': 'POST',
|
|
214
|
-
'access-control-request-headers': 'x-test, content-type',
|
|
215
|
-
},
|
|
216
|
-
}),
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
expect(response.status).toBe(HttpStatus.NO_CONTENT)
|
|
220
|
-
expect(handler).not.toHaveBeenCalled()
|
|
221
|
-
expect(response.headers.get('access-control-allow-origin')).toBe('*')
|
|
222
|
-
expect(response.headers.get('access-control-allow-methods')).toBe(
|
|
223
|
-
'GET, HEAD, PUT, POST, DELETE, PATCH',
|
|
224
|
-
)
|
|
225
|
-
expect(response.headers.get('access-control-allow-headers')).toBe('x-test, content-type')
|
|
226
|
-
expect(response.headers.get('access-control-max-age')).toBe('600')
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('uses custom allowHeaders and preflightStatus', async () => {
|
|
230
|
-
const router = new Router()
|
|
231
|
-
router.use(
|
|
232
|
-
cors({
|
|
233
|
-
origin: '*',
|
|
234
|
-
allowHeaders: ['x-custom', 'content-type'],
|
|
235
|
-
preflightStatus: HttpStatus.OK,
|
|
236
|
-
}),
|
|
237
|
-
)
|
|
238
|
-
router.add({
|
|
239
|
-
method: HttpMethod.OPTIONS,
|
|
240
|
-
path: '/widgets',
|
|
241
|
-
handler: () => new Response('ok'),
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
const response = await router.fetch(
|
|
245
|
-
new Request('https://api.example.com/widgets', {
|
|
246
|
-
method: HttpMethod.OPTIONS,
|
|
247
|
-
headers: {
|
|
248
|
-
origin: 'https://app.example.com',
|
|
249
|
-
'access-control-request-method': 'POST',
|
|
250
|
-
},
|
|
251
|
-
}),
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
expect(response.status).toBe(HttpStatus.OK)
|
|
255
|
-
expect(response.headers.get('access-control-allow-headers')).toBe('x-custom, content-type')
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('declares only applicable preflight route responses', () => {
|
|
259
|
-
const router = new Router().use(cors({ origin: '*' }))
|
|
260
|
-
router.add({ method: HttpMethod.GET, path: '/widgets', handler: () => new Response() })
|
|
261
|
-
router.add({ method: HttpMethod.OPTIONS, path: '/widgets', handler: () => new Response() })
|
|
262
|
-
|
|
263
|
-
const [getRoute, optionsRoute] = router.getRoutes()
|
|
264
|
-
expect(getRoute?.schema).toBeUndefined()
|
|
265
|
-
expect(optionsRoute?.schema?.response?.body).toEqual({
|
|
266
|
-
[HttpStatus.NO_CONTENT]: { type: 'null' },
|
|
267
|
-
})
|
|
268
|
-
})
|
|
269
|
-
})
|