@mpen/routekit 0.1.0 → 0.1.2
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,490 +0,0 @@
|
|
|
1
|
-
import { CommonHeaders, HttpStatus, HttpMethod } from '@mpen/http'
|
|
2
|
-
import type {
|
|
3
|
-
AnyContext,
|
|
4
|
-
HandlerFinalResult,
|
|
5
|
-
HandlerYield,
|
|
6
|
-
MaybePromise,
|
|
7
|
-
OneOrMany,
|
|
8
|
-
RequestContext,
|
|
9
|
-
} from '../types'
|
|
10
|
-
import { isLocalhost } from '../lib/host'
|
|
11
|
-
import type { RouterBodyInit } from '../fetch-types'
|
|
12
|
-
import {
|
|
13
|
-
headers as headersDirective,
|
|
14
|
-
isChunkDirective,
|
|
15
|
-
isHeadersDirective,
|
|
16
|
-
isHeadDirective,
|
|
17
|
-
isResponseBodyInit,
|
|
18
|
-
isRoutekitResponse,
|
|
19
|
-
isStreamDirective,
|
|
20
|
-
response,
|
|
21
|
-
} from '../response'
|
|
22
|
-
import { empty } from '../response/formats/content'
|
|
23
|
-
import { defineMiddleware, type DeclaredMiddleware } from './define'
|
|
24
|
-
|
|
25
|
-
type CorsOriginResolver<Ctx extends object> = (
|
|
26
|
-
origin: string | null,
|
|
27
|
-
ctx: RequestContext<Ctx>,
|
|
28
|
-
) => MaybePromise<string | null | undefined | false>
|
|
29
|
-
|
|
30
|
-
type CorsMethodsResolver<Ctx extends object> = (
|
|
31
|
-
origin: string | null,
|
|
32
|
-
ctx: RequestContext<Ctx>,
|
|
33
|
-
) => MaybePromise<OneOrMany<string>>
|
|
34
|
-
|
|
35
|
-
type AllowedOriginEntry =
|
|
36
|
-
| { kind: 'origin'; value: string }
|
|
37
|
-
| { kind: 'host'; value: string }
|
|
38
|
-
| { kind: 'regex'; value: RegExp }
|
|
39
|
-
| { kind: 'null' }
|
|
40
|
-
|
|
41
|
-
export interface CorsOptions<Ctx extends object = AnyContext> {
|
|
42
|
-
/**
|
|
43
|
-
* Allowed origin(s) for cross-origin requests.
|
|
44
|
-
* Use `'*'` to allow all origins.
|
|
45
|
-
*/
|
|
46
|
-
origin: OneOrMany<string | URL | RegExp> | CorsOriginResolver<Ctx>
|
|
47
|
-
/**
|
|
48
|
-
* Allowed methods to echo in preflight responses.
|
|
49
|
-
*/
|
|
50
|
-
allowMethods?: OneOrMany<string> | CorsMethodsResolver<Ctx>
|
|
51
|
-
/**
|
|
52
|
-
* Allowed headers to echo in preflight responses.
|
|
53
|
-
*/
|
|
54
|
-
allowHeaders?: OneOrMany<string>
|
|
55
|
-
/**
|
|
56
|
-
* Response headers that should be exposed to the browser.
|
|
57
|
-
*/
|
|
58
|
-
exposeHeaders?: OneOrMany<string>
|
|
59
|
-
/**
|
|
60
|
-
* Max age (seconds) for caching preflight responses.
|
|
61
|
-
*/
|
|
62
|
-
maxAge?: number
|
|
63
|
-
/**
|
|
64
|
-
* Whether to set Access-Control-Allow-Credentials.
|
|
65
|
-
*/
|
|
66
|
-
credentials?: boolean
|
|
67
|
-
/**
|
|
68
|
-
* Allow localhost and loopback origins for local development.
|
|
69
|
-
*/
|
|
70
|
-
allowLocalhost?: boolean
|
|
71
|
-
/**
|
|
72
|
-
* Convenience flag that enables localhost allowances.
|
|
73
|
-
*/
|
|
74
|
-
dev?: boolean
|
|
75
|
-
/**
|
|
76
|
-
* HTTP status to use for preflight responses.
|
|
77
|
-
*/
|
|
78
|
-
preflightStatus?: number
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const headerOrigin = CommonHeaders.ORIGIN
|
|
82
|
-
const headerVary = CommonHeaders.VARY
|
|
83
|
-
const headerAccessControlRequestMethod = CommonHeaders.ACCESS_CONTROL_REQUEST_METHOD
|
|
84
|
-
const headerAccessControlRequestHeaders = CommonHeaders.ACCESS_CONTROL_REQUEST_HEADERS
|
|
85
|
-
const headerAllowOrigin = CommonHeaders.ACCESS_CONTROL_ALLOW_ORIGIN
|
|
86
|
-
const headerAllowMethods = CommonHeaders.ACCESS_CONTROL_ALLOW_METHODS
|
|
87
|
-
const headerAllowHeaders = CommonHeaders.ACCESS_CONTROL_ALLOW_HEADERS
|
|
88
|
-
const headerAllowCredentials = CommonHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS
|
|
89
|
-
const headerExposeHeaders = CommonHeaders.ACCESS_CONTROL_EXPOSE_HEADERS
|
|
90
|
-
const headerMaxAge = CommonHeaders.ACCESS_CONTROL_MAX_AGE
|
|
91
|
-
|
|
92
|
-
const defaultAllowedMethods: OneOrMany<string> = [
|
|
93
|
-
HttpMethod.GET,
|
|
94
|
-
HttpMethod.HEAD,
|
|
95
|
-
HttpMethod.PUT,
|
|
96
|
-
HttpMethod.POST,
|
|
97
|
-
HttpMethod.DELETE,
|
|
98
|
-
HttpMethod.PATCH,
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
function normalizeHeaderValue(value: string | null): string | null {
|
|
102
|
-
if (!value) return null
|
|
103
|
-
const trimmed = value.trim()
|
|
104
|
-
return trimmed ? trimmed : null
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function normalizeOriginHeader(value: string | null): string | null {
|
|
108
|
-
return normalizeHeaderValue(value)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function parseOrigin(originHeader: string | null): URL | null {
|
|
112
|
-
if (!originHeader || originHeader === 'null') return null
|
|
113
|
-
try {
|
|
114
|
-
return new URL(originHeader)
|
|
115
|
-
} catch {
|
|
116
|
-
return null
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function normalizeList(value?: OneOrMany<string>): string[] {
|
|
121
|
-
if (!value) return []
|
|
122
|
-
const list = Array.isArray(value) ? value : [value]
|
|
123
|
-
return list.map((entry) => entry.trim()).filter(Boolean)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function normalizeMethods(value?: OneOrMany<string>): string[] {
|
|
127
|
-
return normalizeList(value).map((entry) => entry.toUpperCase())
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function formatHeaderList(value?: OneOrMany<string>): string | null {
|
|
131
|
-
const list = normalizeList(value)
|
|
132
|
-
if (!list.length) return null
|
|
133
|
-
return list.join(', ')
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function formatMethodList(value?: OneOrMany<string>): string | null {
|
|
137
|
-
const list = normalizeMethods(value)
|
|
138
|
-
if (!list.length) return null
|
|
139
|
-
return list.join(', ')
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function normalizeAllowedOrigins(value?: OneOrMany<string | URL | RegExp>): AllowedOriginEntry[] {
|
|
143
|
-
if (!value) return []
|
|
144
|
-
const list = Array.isArray(value) ? value : [value]
|
|
145
|
-
const normalized: AllowedOriginEntry[] = []
|
|
146
|
-
for (const entry of list) {
|
|
147
|
-
if (entry instanceof RegExp) {
|
|
148
|
-
normalized.push({ kind: 'regex', value: entry })
|
|
149
|
-
continue
|
|
150
|
-
}
|
|
151
|
-
if (entry instanceof URL) {
|
|
152
|
-
normalized.push({ kind: 'origin', value: entry.origin })
|
|
153
|
-
continue
|
|
154
|
-
}
|
|
155
|
-
const trimmed = entry.trim()
|
|
156
|
-
if (!trimmed) continue
|
|
157
|
-
if (trimmed === 'null') {
|
|
158
|
-
normalized.push({ kind: 'null' })
|
|
159
|
-
continue
|
|
160
|
-
}
|
|
161
|
-
if (trimmed.includes('://')) {
|
|
162
|
-
try {
|
|
163
|
-
normalized.push({ kind: 'origin', value: new URL(trimmed).origin })
|
|
164
|
-
} catch {
|
|
165
|
-
continue
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
normalized.push({ kind: 'host', value: trimmed.replace(/\/+$/, '') })
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return normalized
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function hasWildcardOrigin(value?: OneOrMany<string | URL | RegExp>): boolean {
|
|
175
|
-
if (!value) return false
|
|
176
|
-
if (!Array.isArray(value)) {
|
|
177
|
-
return typeof value === 'string' && value.trim() === '*'
|
|
178
|
-
}
|
|
179
|
-
return value.some((entry) => typeof entry === 'string' && entry.trim() === '*')
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function isOriginAllowed(
|
|
183
|
-
originHeader: string | null,
|
|
184
|
-
originUrl: URL | null,
|
|
185
|
-
allowlist: AllowedOriginEntry[],
|
|
186
|
-
allowLocalhost: boolean,
|
|
187
|
-
): boolean {
|
|
188
|
-
if (!originHeader) return false
|
|
189
|
-
if (originHeader === 'null') {
|
|
190
|
-
return allowlist.some((entry) => entry.kind === 'null')
|
|
191
|
-
}
|
|
192
|
-
if (originUrl && allowLocalhost && isLocalhost(originUrl.hostname)) return true
|
|
193
|
-
for (const entry of allowlist) {
|
|
194
|
-
if (entry.kind === 'regex') {
|
|
195
|
-
if (entry.value.test(originHeader)) return true
|
|
196
|
-
continue
|
|
197
|
-
}
|
|
198
|
-
if (!originUrl) continue
|
|
199
|
-
if (entry.kind === 'origin') {
|
|
200
|
-
if (originUrl.origin === entry.value) return true
|
|
201
|
-
continue
|
|
202
|
-
}
|
|
203
|
-
if (entry.kind === 'host') {
|
|
204
|
-
if (entry.value.includes(':')) {
|
|
205
|
-
if (originUrl.host === entry.value) return true
|
|
206
|
-
} else if (originUrl.hostname === entry.value) {
|
|
207
|
-
return true
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return false
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function addVaryHeader(headers: Headers, value: string): void {
|
|
215
|
-
const current = headers.get(headerVary)
|
|
216
|
-
if (!current) {
|
|
217
|
-
headers.set(headerVary, value)
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
const existing = current.split(',').map((entry) => entry.trim().toLowerCase())
|
|
221
|
-
if (existing.includes(value.toLowerCase())) return
|
|
222
|
-
headers.set(headerVary, `${current}, ${value}`)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function applyCorsHeaders(
|
|
226
|
-
headers: Headers,
|
|
227
|
-
allowOrigin: string,
|
|
228
|
-
allowCredentials: boolean,
|
|
229
|
-
exposeHeaders?: OneOrMany<string>,
|
|
230
|
-
varyOrigin?: boolean,
|
|
231
|
-
): void {
|
|
232
|
-
headers.set(headerAllowOrigin, allowOrigin)
|
|
233
|
-
if (allowCredentials) {
|
|
234
|
-
headers.set(headerAllowCredentials, 'true')
|
|
235
|
-
}
|
|
236
|
-
const expose = formatHeaderList(exposeHeaders)
|
|
237
|
-
if (expose) {
|
|
238
|
-
headers.set(headerExposeHeaders, expose)
|
|
239
|
-
}
|
|
240
|
-
if (varyOrigin) {
|
|
241
|
-
addVaryHeader(headers, 'Origin')
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function isAsyncGenerator(
|
|
246
|
-
value: unknown,
|
|
247
|
-
): value is AsyncGenerator<HandlerYield, HandlerFinalResult> {
|
|
248
|
-
return (
|
|
249
|
-
!!value &&
|
|
250
|
-
typeof (value as AsyncGenerator<HandlerYield, HandlerFinalResult>)[Symbol.asyncIterator] ===
|
|
251
|
-
'function'
|
|
252
|
-
)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function wrapGeneratorWithCors(
|
|
256
|
-
generator: AsyncGenerator<HandlerYield, HandlerFinalResult>,
|
|
257
|
-
allowOrigin: string,
|
|
258
|
-
allowCredentials: boolean,
|
|
259
|
-
exposeHeaders: OneOrMany<string> | undefined,
|
|
260
|
-
varyOrigin: boolean,
|
|
261
|
-
): AsyncGenerator<HandlerYield, HandlerFinalResult> {
|
|
262
|
-
const apply = (headers: Headers) => {
|
|
263
|
-
applyCorsHeaders(headers, allowOrigin, allowCredentials, exposeHeaders, varyOrigin)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function* wrapped(): AsyncGenerator<HandlerYield, HandlerFinalResult> {
|
|
267
|
-
let headersInjected = false
|
|
268
|
-
while (true) {
|
|
269
|
-
const next = await generator.next()
|
|
270
|
-
if (next.done) {
|
|
271
|
-
if (!headersInjected) {
|
|
272
|
-
const headers = new Headers()
|
|
273
|
-
apply(headers)
|
|
274
|
-
yield headersDirective(headers)
|
|
275
|
-
}
|
|
276
|
-
return next.value
|
|
277
|
-
}
|
|
278
|
-
const value = next.value
|
|
279
|
-
if (isHeadersDirective(value)) {
|
|
280
|
-
const headers = new Headers(value.headers)
|
|
281
|
-
apply(headers)
|
|
282
|
-
headersInjected = true
|
|
283
|
-
yield headersDirective(headers)
|
|
284
|
-
continue
|
|
285
|
-
}
|
|
286
|
-
if (isHeadDirective(value)) {
|
|
287
|
-
const headers = new Headers(value.headers)
|
|
288
|
-
apply(headers)
|
|
289
|
-
headersInjected = true
|
|
290
|
-
yield { ...value, headers }
|
|
291
|
-
continue
|
|
292
|
-
}
|
|
293
|
-
if (isStreamDirective(value)) {
|
|
294
|
-
const headers = new Headers(value.headers)
|
|
295
|
-
apply(headers)
|
|
296
|
-
headersInjected = true
|
|
297
|
-
yield { ...value, headers }
|
|
298
|
-
continue
|
|
299
|
-
}
|
|
300
|
-
if (!headersInjected && isChunkDirective(value)) {
|
|
301
|
-
const headers = new Headers()
|
|
302
|
-
apply(headers)
|
|
303
|
-
headersInjected = true
|
|
304
|
-
yield headersDirective(headers)
|
|
305
|
-
}
|
|
306
|
-
yield value
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return wrapped()
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Attach CORS response headers and handle OPTIONS preflight requests.
|
|
315
|
-
*
|
|
316
|
-
* @example
|
|
317
|
-
* ```ts
|
|
318
|
-
* router.use(cors({origin: '*'}))
|
|
319
|
-
* router.use(cors({origin: ['https://app.example.com'], credentials: true}))
|
|
320
|
-
* router.use(cors({origin: 'https://app.example.com', dev: true}))
|
|
321
|
-
* ```
|
|
322
|
-
*
|
|
323
|
-
* @param options - Configuration for origin, preflight, and header behavior.
|
|
324
|
-
* @returns Middleware that applies CORS headers to matching requests.
|
|
325
|
-
*/
|
|
326
|
-
export function cors<Ctx extends object = AnyContext>(
|
|
327
|
-
options: CorsOptions<Ctx>,
|
|
328
|
-
): DeclaredMiddleware<{}, Ctx> {
|
|
329
|
-
const allowCredentials = options.credentials ?? false
|
|
330
|
-
const allowLocalhost = options.allowLocalhost ?? options.dev ?? false
|
|
331
|
-
const originOption = options.origin
|
|
332
|
-
const originResolver = typeof originOption === 'function' ? originOption : undefined
|
|
333
|
-
const originList = originResolver
|
|
334
|
-
? undefined
|
|
335
|
-
: (originOption as OneOrMany<string | URL | RegExp>)
|
|
336
|
-
const allowlist = originList ? normalizeAllowedOrigins(originList) : []
|
|
337
|
-
const hasWildcard = originList ? hasWildcardOrigin(originList) : false
|
|
338
|
-
const preflightStatus = options.preflightStatus ?? HttpStatus.NO_CONTENT
|
|
339
|
-
|
|
340
|
-
return defineMiddleware({
|
|
341
|
-
responses: {
|
|
342
|
-
[preflightStatus]: {
|
|
343
|
-
schema: { type: 'null' },
|
|
344
|
-
parse(value: unknown): undefined {
|
|
345
|
-
if (value !== undefined) {
|
|
346
|
-
throw new TypeError('CORS preflight responses must not contain a body.')
|
|
347
|
-
}
|
|
348
|
-
return undefined
|
|
349
|
-
},
|
|
350
|
-
},
|
|
351
|
-
},
|
|
352
|
-
schemaAppliesTo: ({ method }) =>
|
|
353
|
-
Array.isArray(method)
|
|
354
|
-
? method.includes(HttpMethod.OPTIONS)
|
|
355
|
-
: method === HttpMethod.OPTIONS,
|
|
356
|
-
async run(ctx, { next, forward, respond }) {
|
|
357
|
-
const originHeader = normalizeOriginHeader(ctx.request.headers.get(headerOrigin))
|
|
358
|
-
const originUrl = parseOrigin(originHeader)
|
|
359
|
-
|
|
360
|
-
let allowOrigin: string | null = null
|
|
361
|
-
let varyOrigin = false
|
|
362
|
-
|
|
363
|
-
if (originResolver) {
|
|
364
|
-
const resolved = await originResolver(originHeader, ctx)
|
|
365
|
-
if (resolved) {
|
|
366
|
-
const normalized = String(resolved).trim()
|
|
367
|
-
if (normalized === '*') {
|
|
368
|
-
if (allowCredentials && originHeader) {
|
|
369
|
-
allowOrigin = originUrl?.origin ?? originHeader
|
|
370
|
-
varyOrigin = true
|
|
371
|
-
} else if (!allowCredentials) {
|
|
372
|
-
allowOrigin = '*'
|
|
373
|
-
}
|
|
374
|
-
} else if (normalized) {
|
|
375
|
-
allowOrigin = normalized
|
|
376
|
-
varyOrigin = Boolean(originHeader)
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
} else if (hasWildcard) {
|
|
380
|
-
if (allowCredentials && originHeader) {
|
|
381
|
-
allowOrigin = originUrl?.origin ?? originHeader
|
|
382
|
-
varyOrigin = true
|
|
383
|
-
} else if (!allowCredentials) {
|
|
384
|
-
allowOrigin = '*'
|
|
385
|
-
}
|
|
386
|
-
} else if (
|
|
387
|
-
originHeader &&
|
|
388
|
-
isOriginAllowed(originHeader, originUrl, allowlist, allowLocalhost)
|
|
389
|
-
) {
|
|
390
|
-
allowOrigin = originUrl?.origin ?? originHeader
|
|
391
|
-
varyOrigin = true
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const isPreflight =
|
|
395
|
-
ctx.request.method.toUpperCase() === 'OPTIONS' &&
|
|
396
|
-
ctx.request.headers.has(headerAccessControlRequestMethod)
|
|
397
|
-
|
|
398
|
-
if (isPreflight) {
|
|
399
|
-
if (!allowOrigin) {
|
|
400
|
-
return respond(empty(preflightStatus))
|
|
401
|
-
}
|
|
402
|
-
const headers = new Headers()
|
|
403
|
-
applyCorsHeaders(headers, allowOrigin, allowCredentials, undefined, varyOrigin)
|
|
404
|
-
|
|
405
|
-
const allowMethods =
|
|
406
|
-
typeof options.allowMethods === 'function'
|
|
407
|
-
? await options.allowMethods(originHeader, ctx)
|
|
408
|
-
: (options.allowMethods ?? defaultAllowedMethods)
|
|
409
|
-
const allowMethodsValue = formatMethodList(allowMethods)
|
|
410
|
-
if (allowMethodsValue) {
|
|
411
|
-
headers.set(headerAllowMethods, allowMethodsValue)
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const requestHeaders = normalizeHeaderValue(
|
|
415
|
-
ctx.request.headers.get(headerAccessControlRequestHeaders),
|
|
416
|
-
)
|
|
417
|
-
const allowHeadersValue = formatHeaderList(
|
|
418
|
-
options.allowHeaders ?? requestHeaders ?? undefined,
|
|
419
|
-
)
|
|
420
|
-
if (allowHeadersValue) {
|
|
421
|
-
headers.set(headerAllowHeaders, allowHeadersValue)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (options.maxAge != null) {
|
|
425
|
-
headers.set(headerMaxAge, String(options.maxAge))
|
|
426
|
-
}
|
|
427
|
-
return respond(empty(preflightStatus, { headers }))
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const result = await next()
|
|
431
|
-
if (!allowOrigin) return forward(result)
|
|
432
|
-
|
|
433
|
-
if (result instanceof Response) {
|
|
434
|
-
applyCorsHeaders(
|
|
435
|
-
result.headers,
|
|
436
|
-
allowOrigin,
|
|
437
|
-
allowCredentials,
|
|
438
|
-
options.exposeHeaders,
|
|
439
|
-
varyOrigin,
|
|
440
|
-
)
|
|
441
|
-
return forward(result)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (isRoutekitResponse(result)) {
|
|
445
|
-
applyCorsHeaders(
|
|
446
|
-
result.headers,
|
|
447
|
-
allowOrigin,
|
|
448
|
-
allowCredentials,
|
|
449
|
-
options.exposeHeaders,
|
|
450
|
-
varyOrigin,
|
|
451
|
-
)
|
|
452
|
-
return forward(result)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (isAsyncGenerator(result)) {
|
|
456
|
-
return forward(
|
|
457
|
-
wrapGeneratorWithCors(
|
|
458
|
-
result,
|
|
459
|
-
allowOrigin,
|
|
460
|
-
allowCredentials,
|
|
461
|
-
options.exposeHeaders,
|
|
462
|
-
varyOrigin,
|
|
463
|
-
),
|
|
464
|
-
)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (result != null && isResponseBodyInit(result)) {
|
|
468
|
-
const headers = new Headers()
|
|
469
|
-
applyCorsHeaders(
|
|
470
|
-
headers,
|
|
471
|
-
allowOrigin,
|
|
472
|
-
allowCredentials,
|
|
473
|
-
options.exposeHeaders,
|
|
474
|
-
varyOrigin,
|
|
475
|
-
)
|
|
476
|
-
return forward(new Response(result as RouterBodyInit, { headers }))
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const headers = new Headers()
|
|
480
|
-
applyCorsHeaders(
|
|
481
|
-
headers,
|
|
482
|
-
allowOrigin,
|
|
483
|
-
allowCredentials,
|
|
484
|
-
options.exposeHeaders,
|
|
485
|
-
varyOrigin,
|
|
486
|
-
)
|
|
487
|
-
return forward(response(result, { headers }))
|
|
488
|
-
},
|
|
489
|
-
})
|
|
490
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun test
|
|
2
|
-
import { describe, expect, it } from 'bun:test'
|
|
3
|
-
import { HttpMethod, HttpStatus } from '@mpen/http'
|
|
4
|
-
import { Router } from '../router'
|
|
5
|
-
import { csrf } from './csrf'
|
|
6
|
-
|
|
7
|
-
describe(csrf.name, () => {
|
|
8
|
-
it('allows same-site fetch requests by default', async () => {
|
|
9
|
-
const router = new Router()
|
|
10
|
-
router.use(csrf())
|
|
11
|
-
router.add({
|
|
12
|
-
method: HttpMethod.POST,
|
|
13
|
-
path: '/submit',
|
|
14
|
-
handler: () => new Response('ok'),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const request = new Request('https://api.example.com/submit', {
|
|
18
|
-
method: HttpMethod.POST,
|
|
19
|
-
headers: {
|
|
20
|
-
origin: 'https://app.example.com',
|
|
21
|
-
'sec-fetch-dest': 'empty',
|
|
22
|
-
'sec-fetch-mode': 'cors',
|
|
23
|
-
'sec-fetch-site': 'same-site',
|
|
24
|
-
},
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
const response = await router.fetch(request)
|
|
28
|
-
|
|
29
|
-
expect(response.status).toBe(HttpStatus.OK)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('rejects cross-site origins by default', async () => {
|
|
33
|
-
const router = new Router()
|
|
34
|
-
router.use(csrf())
|
|
35
|
-
router.add({
|
|
36
|
-
method: HttpMethod.POST,
|
|
37
|
-
path: '/submit',
|
|
38
|
-
handler: () => new Response('ok'),
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const request = new Request('https://api.example.com/submit', {
|
|
42
|
-
method: HttpMethod.POST,
|
|
43
|
-
headers: {
|
|
44
|
-
origin: 'https://evil.example',
|
|
45
|
-
'sec-fetch-dest': 'empty',
|
|
46
|
-
'sec-fetch-mode': 'cors',
|
|
47
|
-
'sec-fetch-site': 'cross-site',
|
|
48
|
-
},
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
const response = await router.fetch(request)
|
|
52
|
-
|
|
53
|
-
expect(response.status).toBe(HttpStatus.FORBIDDEN)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('allows whitelisted origins even when cross-site', async () => {
|
|
57
|
-
const router = new Router()
|
|
58
|
-
router.use(csrf({ allowedOrigins: ['https://evil.example'] }))
|
|
59
|
-
router.add({
|
|
60
|
-
method: HttpMethod.POST,
|
|
61
|
-
path: '/submit',
|
|
62
|
-
handler: () => new Response('ok'),
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
const request = new Request('https://api.example.com/submit', {
|
|
66
|
-
method: HttpMethod.POST,
|
|
67
|
-
headers: {
|
|
68
|
-
origin: 'https://evil.example',
|
|
69
|
-
'sec-fetch-dest': 'empty',
|
|
70
|
-
'sec-fetch-mode': 'cors',
|
|
71
|
-
'sec-fetch-site': 'cross-site',
|
|
72
|
-
},
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
const response = await router.fetch(request)
|
|
76
|
-
|
|
77
|
-
expect(response.status).toBe(HttpStatus.OK)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('allows local dev requests without fetch metadata or origin when dev is enabled', async () => {
|
|
81
|
-
const router = new Router()
|
|
82
|
-
router.use(csrf({ dev: true }))
|
|
83
|
-
router.add({
|
|
84
|
-
method: HttpMethod.POST,
|
|
85
|
-
path: '/submit',
|
|
86
|
-
handler: () => new Response('ok'),
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
const request = new Request('http://localhost:8787/submit', {
|
|
90
|
-
method: HttpMethod.POST,
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
const response = await router.fetch(request)
|
|
94
|
-
|
|
95
|
-
expect(response.status).toBe(HttpStatus.OK)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('declares its rejection response on affected routes', () => {
|
|
99
|
-
const router = new Router().use(csrf())
|
|
100
|
-
router.add({ method: HttpMethod.POST, path: '/submit', handler: () => new Response() })
|
|
101
|
-
|
|
102
|
-
expect(router.getRoutes()[0]?.schema?.response?.body).toEqual({
|
|
103
|
-
[HttpStatus.FORBIDDEN]: { type: 'string' },
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
})
|