@bunary/http 0.2.0 → 0.3.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/CHANGELOG.md +20 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/context.d.ts +13 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/cors.d.ts +82 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/handlers/methodNotAllowed.d.ts.map +1 -1
- package/dist/handlers/notFound.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +143 -19
- package/dist/routes/find.d.ts +7 -0
- package/dist/routes/find.d.ts.map +1 -1
- package/dist/routes/index.d.ts +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/types/requestContext.d.ts +54 -0
- package/dist/types/requestContext.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to `@bunary/http` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-02-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Built-in CORS middleware via `cors()` factory (#47)
|
|
13
|
+
- `cors()` with no arguments allows all origins (default `Access-Control-Allow-Origin: *`)
|
|
14
|
+
- Configurable `origin` (string, string array, or `"*"`), `methods`, `allowHeaders`, `exposeHeaders`, `credentials`, `maxAge`
|
|
15
|
+
- Handles preflight `OPTIONS` requests automatically — returns 204 with CORS headers
|
|
16
|
+
- Reflects `Access-Control-Request-Headers` by default, or uses explicit `allowHeaders`
|
|
17
|
+
- Adds `Vary: Origin` when origin is not `"*"` for correct cache behavior
|
|
18
|
+
- Works as global middleware (`app.use(cors())`) or per-group (`middleware: [cors()]`)
|
|
19
|
+
- `CorsOptions` type exported for TypeScript consumers
|
|
20
|
+
|
|
21
|
+
- Body parsing helpers on `RequestContext` — `ctx.json()`, `ctx.text()`, `ctx.formData()` (#51)
|
|
22
|
+
- `ctx.json<T>()` — parse JSON body with type inference, throws `BodyParseError` on malformed input
|
|
23
|
+
- `ctx.text()` — get request body as string
|
|
24
|
+
- `ctx.formData()` — parse multipart/URL-encoded form data, throws `BodyParseError` on malformed input
|
|
25
|
+
- `BodyParseError` class exported for catch-based error handling in handlers
|
|
26
|
+
- Thin wrappers around `Request` methods — no parsing framework, no validation, zero new dependencies
|
|
27
|
+
|
|
8
28
|
## [0.2.0] - 2026-02-15
|
|
9
29
|
|
|
10
30
|
### Added
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACX,UAAU,EACV,SAAS,EAYT,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAC3B,SAAS,CAAC,OAAO,CAAC,CAkSpB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PathParams } from "./types/pathParams.js";
|
|
2
|
+
import type { RequestContext } from "./types/requestContext.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create a RequestContext for a given request and params.
|
|
5
|
+
*
|
|
6
|
+
* Centralises context construction so that body helpers (`json`, `text`,
|
|
7
|
+
* `formData`) are available everywhere a `RequestContext` is built —
|
|
8
|
+
* including 404/405 handler contexts.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function createRequestContext(request: Request, params: PathParams, query: URLSearchParams): RequestContext;
|
|
13
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhE;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,eAAe,GACpB,cAAc,CAsBhB"}
|
package/dist/cors.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Middleware } from "./types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* CORS configuration options.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const options: CorsOptions = {
|
|
8
|
+
* origin: "https://myapp.com",
|
|
9
|
+
* methods: ["GET", "POST"],
|
|
10
|
+
* credentials: true,
|
|
11
|
+
* maxAge: 86400,
|
|
12
|
+
* };
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export interface CorsOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Allowed origin(s). Use `"*"` (default) for any origin,
|
|
18
|
+
* a single string for one origin, or an array for multiple.
|
|
19
|
+
*
|
|
20
|
+
* @default "*"
|
|
21
|
+
*/
|
|
22
|
+
origin?: string | string[];
|
|
23
|
+
/**
|
|
24
|
+
* HTTP methods to advertise in `Access-Control-Allow-Methods`.
|
|
25
|
+
*
|
|
26
|
+
* @default ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]
|
|
27
|
+
*/
|
|
28
|
+
methods?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* Headers the client is allowed to send.
|
|
31
|
+
* When omitted, the value of `Access-Control-Request-Headers` is reflected.
|
|
32
|
+
*/
|
|
33
|
+
allowHeaders?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Response headers the browser may expose to client-side JavaScript.
|
|
36
|
+
*/
|
|
37
|
+
exposeHeaders?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Whether to include `Access-Control-Allow-Credentials: true`.
|
|
40
|
+
*
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
credentials?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* How long (in seconds) the browser may cache preflight results.
|
|
46
|
+
* Omitted from the response when `undefined`.
|
|
47
|
+
*/
|
|
48
|
+
maxAge?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a CORS middleware.
|
|
52
|
+
*
|
|
53
|
+
* Handles preflight `OPTIONS` requests (returns 204) and
|
|
54
|
+
* adds CORS headers to actual responses.
|
|
55
|
+
*
|
|
56
|
+
* @param options - CORS configuration (defaults allow all origins)
|
|
57
|
+
* @returns Middleware function
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { createApp, cors } from "@bunary/http";
|
|
62
|
+
*
|
|
63
|
+
* // Allow any origin
|
|
64
|
+
* const app = createApp();
|
|
65
|
+
* app.use(cors());
|
|
66
|
+
*
|
|
67
|
+
* // Restrict to a single origin with credentials
|
|
68
|
+
* app.use(cors({
|
|
69
|
+
* origin: "https://myapp.com",
|
|
70
|
+
* credentials: true,
|
|
71
|
+
* maxAge: 86400,
|
|
72
|
+
* }));
|
|
73
|
+
*
|
|
74
|
+
* // Multiple allowed origins
|
|
75
|
+
* app.use(cors({
|
|
76
|
+
* origin: ["https://app1.com", "https://app2.com"],
|
|
77
|
+
* methods: ["GET", "POST"],
|
|
78
|
+
* }));
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function cors(options?: CorsOptions): Middleware;
|
|
82
|
+
//# sourceMappingURL=cors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,WAAW;IAC3B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE3B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAEzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AA0BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CAwF1D"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when request body parsing fails.
|
|
3
|
+
*
|
|
4
|
+
* Thrown by `ctx.json()` and `ctx.formData()` when the request body
|
|
5
|
+
* cannot be parsed. Handlers can catch this to return a custom 400 response.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { BodyParseError } from "@bunary/http";
|
|
10
|
+
*
|
|
11
|
+
* app.post("/users", async (ctx) => {
|
|
12
|
+
* try {
|
|
13
|
+
* const body = await ctx.json();
|
|
14
|
+
* return { received: body };
|
|
15
|
+
* } catch (error) {
|
|
16
|
+
* if (error instanceof BodyParseError) {
|
|
17
|
+
* return new Response(JSON.stringify({ error: error.message }), {
|
|
18
|
+
* status: 400,
|
|
19
|
+
* headers: { "Content-Type": "application/json" },
|
|
20
|
+
* });
|
|
21
|
+
* }
|
|
22
|
+
* throw error;
|
|
23
|
+
* }
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare class BodyParseError extends Error {
|
|
28
|
+
readonly name = "BodyParseError";
|
|
29
|
+
/** The underlying parse error, if available */
|
|
30
|
+
readonly cause?: unknown;
|
|
31
|
+
constructor(message: string, cause?: unknown);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACxC,SAAkB,IAAI,oBAAoB;IAE1C,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI5C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"methodNotAllowed.d.ts","sourceRoot":"","sources":["../../src/handlers/methodNotAllowed.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"methodNotAllowed.d.ts","sourceRoot":"","sources":["../../src/handlers/methodNotAllowed.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAkB,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE3E;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC3C,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,KAAK,EAAE,EACf,OAAO,CAAC,EAAE,UAAU,EACpB,WAAW,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,QAAQ,CAAC,CA2BnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notFound.d.ts","sourceRoot":"","sources":["../../src/handlers/notFound.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"notFound.d.ts","sourceRoot":"","sources":["../../src/handlers/notFound.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAC;AAEpE;;;GAGG;AACH,wBAAsB,cAAc,CACnC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,QAAQ,CAAC,CAWnB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,5 +19,8 @@
|
|
|
19
19
|
* @packageDocumentation
|
|
20
20
|
*/
|
|
21
21
|
export { createApp } from "./app.js";
|
|
22
|
+
export type { CorsOptions } from "./cors.js";
|
|
23
|
+
export { cors } from "./cors.js";
|
|
24
|
+
export { BodyParseError } from "./errors.js";
|
|
22
25
|
export type { AppOptions, BunaryApp, BunaryServer, GroupCallback, GroupOptions, GroupRouter, HandlerResponse, HttpMethod, ListenOptions, Middleware, PathParams, RequestContext, RouteBuilder, RouteHandler, RouteInfo, } from "./types/index.js";
|
|
23
26
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,YAAY,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,EACb,UAAU,EACV,UAAU,EACV,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,GACT,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// src/errors.ts
|
|
3
|
+
class BodyParseError extends Error {
|
|
4
|
+
name = "BodyParseError";
|
|
5
|
+
cause;
|
|
6
|
+
constructor(message, cause) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.cause = cause;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/context.ts
|
|
13
|
+
function createRequestContext(request, params, query) {
|
|
14
|
+
return {
|
|
15
|
+
request,
|
|
16
|
+
params,
|
|
17
|
+
query,
|
|
18
|
+
locals: {},
|
|
19
|
+
json: async () => {
|
|
20
|
+
try {
|
|
21
|
+
return await request.json();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new BodyParseError("Failed to parse JSON body", error);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
text: () => request.text(),
|
|
27
|
+
formData: async () => {
|
|
28
|
+
try {
|
|
29
|
+
return await request.formData();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new BodyParseError("Failed to parse form data", error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
2
37
|
// src/response.ts
|
|
3
38
|
function toResponse(result) {
|
|
4
39
|
if (result instanceof Response) {
|
|
@@ -259,6 +294,17 @@ function resolveRoute(routes, method, path) {
|
|
|
259
294
|
allowedMethods: Array.from(allowedMethods).sort()
|
|
260
295
|
};
|
|
261
296
|
}
|
|
297
|
+
function findRouteByPath(routes, path) {
|
|
298
|
+
for (const route of routes) {
|
|
299
|
+
if (!route.pattern.test(path))
|
|
300
|
+
continue;
|
|
301
|
+
const params = extractParams(path, route);
|
|
302
|
+
if (!checkConstraints(params, route.constraints))
|
|
303
|
+
continue;
|
|
304
|
+
return { route, params };
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
262
308
|
function getAllowedMethods(routes, path) {
|
|
263
309
|
const methods = new Set;
|
|
264
310
|
for (const route of routes) {
|
|
@@ -345,12 +391,7 @@ function toHeadResponse(response) {
|
|
|
345
391
|
async function handleMethodNotAllowed(request, path, routes, options, precomputed) {
|
|
346
392
|
const url = new URL(request.url);
|
|
347
393
|
const allowedMethods = precomputed ?? getAllowedMethods(routes, path);
|
|
348
|
-
const methodNotAllowedCtx = {
|
|
349
|
-
request,
|
|
350
|
-
params: {},
|
|
351
|
-
query: url.searchParams,
|
|
352
|
-
locals: {}
|
|
353
|
-
};
|
|
394
|
+
const methodNotAllowedCtx = createRequestContext(request, {}, url.searchParams);
|
|
354
395
|
if (options?.onMethodNotAllowed) {
|
|
355
396
|
const result = await options.onMethodNotAllowed(methodNotAllowedCtx, allowedMethods);
|
|
356
397
|
const response = toResponse(result);
|
|
@@ -377,12 +418,7 @@ async function handleMethodNotAllowed(request, path, routes, options, precompute
|
|
|
377
418
|
// src/handlers/notFound.ts
|
|
378
419
|
async function handleNotFound(request, _path, options) {
|
|
379
420
|
const url = new URL(request.url);
|
|
380
|
-
const notFoundCtx = {
|
|
381
|
-
request,
|
|
382
|
-
params: {},
|
|
383
|
-
query: url.searchParams,
|
|
384
|
-
locals: {}
|
|
385
|
-
};
|
|
421
|
+
const notFoundCtx = createRequestContext(request, {}, url.searchParams);
|
|
386
422
|
if (options?.onNotFound) {
|
|
387
423
|
const result = await options.onNotFound(notFoundCtx);
|
|
388
424
|
return toResponse(result);
|
|
@@ -457,6 +493,24 @@ function createApp(options) {
|
|
|
457
493
|
const path = url.pathname;
|
|
458
494
|
const method = request.method;
|
|
459
495
|
if (method === "OPTIONS") {
|
|
496
|
+
if (request.headers.get("Origin")) {
|
|
497
|
+
const routeMatch = findRouteByPath(routes, path);
|
|
498
|
+
const chain = routeMatch ? getMiddlewareChain(routeMatch.route) : middlewares.length > 0 ? [...middlewares] : [];
|
|
499
|
+
if (chain.length > 0) {
|
|
500
|
+
const params = routeMatch?.params ?? {};
|
|
501
|
+
const ctx2 = createRequestContext(request, params, url.searchParams);
|
|
502
|
+
let index = 0;
|
|
503
|
+
const next = async () => {
|
|
504
|
+
if (index < chain.length) {
|
|
505
|
+
const mw = chain[index++];
|
|
506
|
+
return await mw(ctx2, next);
|
|
507
|
+
}
|
|
508
|
+
return await handleOptions(request, path, routes, internalOpts);
|
|
509
|
+
};
|
|
510
|
+
const result = await next();
|
|
511
|
+
return toResponse(result);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
460
514
|
return await handleOptions(request, path, routes, internalOpts);
|
|
461
515
|
}
|
|
462
516
|
const { match, allowedMethods } = resolveRoute(routes, method, path);
|
|
@@ -466,12 +520,7 @@ function createApp(options) {
|
|
|
466
520
|
}
|
|
467
521
|
return await handleNotFound(request, path, internalOpts);
|
|
468
522
|
}
|
|
469
|
-
const ctx =
|
|
470
|
-
request,
|
|
471
|
-
params: match.params,
|
|
472
|
-
query: url.searchParams,
|
|
473
|
-
locals: {}
|
|
474
|
-
};
|
|
523
|
+
const ctx = createRequestContext(request, match.params, url.searchParams);
|
|
475
524
|
try {
|
|
476
525
|
const response = await executeRoute(match, ctx, getMiddlewareChain);
|
|
477
526
|
if (method === "HEAD") {
|
|
@@ -589,6 +638,81 @@ function createApp(options) {
|
|
|
589
638
|
};
|
|
590
639
|
return app;
|
|
591
640
|
}
|
|
641
|
+
// src/cors.ts
|
|
642
|
+
var DEFAULT_METHODS = ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"];
|
|
643
|
+
function resolveOrigin(allowed, requestOrigin, credentials) {
|
|
644
|
+
if (allowed === "*") {
|
|
645
|
+
return credentials ? requestOrigin : "*";
|
|
646
|
+
}
|
|
647
|
+
if (typeof allowed === "string") {
|
|
648
|
+
return allowed === requestOrigin ? allowed : null;
|
|
649
|
+
}
|
|
650
|
+
return allowed.includes(requestOrigin) ? requestOrigin : null;
|
|
651
|
+
}
|
|
652
|
+
function cors(options = {}) {
|
|
653
|
+
const {
|
|
654
|
+
origin = "*",
|
|
655
|
+
methods = DEFAULT_METHODS,
|
|
656
|
+
allowHeaders,
|
|
657
|
+
exposeHeaders,
|
|
658
|
+
credentials = false,
|
|
659
|
+
maxAge
|
|
660
|
+
} = options;
|
|
661
|
+
return async (ctx, next) => {
|
|
662
|
+
const requestOrigin = ctx.request.headers.get("Origin");
|
|
663
|
+
if (!requestOrigin) {
|
|
664
|
+
return await next();
|
|
665
|
+
}
|
|
666
|
+
const allowedOrigin = resolveOrigin(origin, requestOrigin, credentials);
|
|
667
|
+
if (!allowedOrigin) {
|
|
668
|
+
return await next();
|
|
669
|
+
}
|
|
670
|
+
const needsVary = origin !== "*" || credentials;
|
|
671
|
+
if (ctx.request.method === "OPTIONS") {
|
|
672
|
+
const headers = new Headers;
|
|
673
|
+
headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
674
|
+
if (needsVary) {
|
|
675
|
+
headers.append("Vary", "Origin");
|
|
676
|
+
}
|
|
677
|
+
headers.set("Access-Control-Allow-Methods", methods.join(", "));
|
|
678
|
+
if (allowHeaders) {
|
|
679
|
+
headers.set("Access-Control-Allow-Headers", allowHeaders.join(", "));
|
|
680
|
+
} else {
|
|
681
|
+
const requested = ctx.request.headers.get("Access-Control-Request-Headers");
|
|
682
|
+
if (requested) {
|
|
683
|
+
headers.set("Access-Control-Allow-Headers", requested);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (credentials) {
|
|
687
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
688
|
+
}
|
|
689
|
+
if (maxAge !== undefined) {
|
|
690
|
+
headers.set("Access-Control-Max-Age", String(maxAge));
|
|
691
|
+
}
|
|
692
|
+
return new Response(null, { status: 204, headers });
|
|
693
|
+
}
|
|
694
|
+
const result = await next();
|
|
695
|
+
const response = toResponse(result);
|
|
696
|
+
const newHeaders = new Headers(response.headers);
|
|
697
|
+
newHeaders.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
698
|
+
if (needsVary) {
|
|
699
|
+
newHeaders.append("Vary", "Origin");
|
|
700
|
+
}
|
|
701
|
+
if (exposeHeaders && exposeHeaders.length > 0) {
|
|
702
|
+
newHeaders.set("Access-Control-Expose-Headers", exposeHeaders.join(", "));
|
|
703
|
+
}
|
|
704
|
+
if (credentials) {
|
|
705
|
+
newHeaders.set("Access-Control-Allow-Credentials", "true");
|
|
706
|
+
}
|
|
707
|
+
return new Response(response.body, {
|
|
708
|
+
status: response.status,
|
|
709
|
+
statusText: response.statusText,
|
|
710
|
+
headers: newHeaders
|
|
711
|
+
});
|
|
712
|
+
};
|
|
713
|
+
}
|
|
592
714
|
export {
|
|
593
|
-
createApp
|
|
715
|
+
createApp,
|
|
716
|
+
cors,
|
|
717
|
+
BodyParseError
|
|
594
718
|
};
|
package/dist/routes/find.d.ts
CHANGED
|
@@ -47,6 +47,13 @@ export declare function findRoute(routes: Route[], method: string, path: string)
|
|
|
47
47
|
* Check if any route matches the path (regardless of method).
|
|
48
48
|
*/
|
|
49
49
|
export declare function hasMatchingPath(routes: Route[], path: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Find the first route whose path matches, regardless of HTTP method.
|
|
52
|
+
*
|
|
53
|
+
* Used by the OPTIONS/CORS preflight handler to locate group middleware
|
|
54
|
+
* attached to a route at this path.
|
|
55
|
+
*/
|
|
56
|
+
export declare function findRouteByPath(routes: Route[], path: string): RouteMatch | null;
|
|
50
57
|
/**
|
|
51
58
|
* Get all allowed HTTP methods for a given path.
|
|
52
59
|
* Respects route constraints when determining matches.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/routes/find.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3C;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC/B,kEAAkE;IAClE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,oGAAoG;IACpG,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CAiC3F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAc1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAMtE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAYzE"}
|
|
1
|
+
{"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/routes/find.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3C;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC/B,kEAAkE;IAClE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,oGAAoG;IACpG,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CAiC3F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAc1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAMtE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAQhF;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAYzE"}
|
package/dist/routes/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { compilePattern, createRouteBuilder, wrapBuilderWithNamePrefix } from "./builder.js";
|
|
2
|
-
export { findRoute, getAllowedMethods, hasMatchingPath, type RouteMatch, type RouteResolution, resolveRoute, } from "./find.js";
|
|
2
|
+
export { findRoute, findRouteByPath, getAllowedMethods, hasMatchingPath, type RouteMatch, type RouteResolution, resolveRoute, } from "./find.js";
|
|
3
3
|
export { type AddRouteFn, createGroupRouter } from "./group.js";
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAC7F,OAAO,EACN,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,YAAY,GACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,KAAK,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAC7F,OAAO,EACN,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,YAAY,GACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,KAAK,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -43,5 +43,59 @@ export interface RequestContext<TLocals extends object = Record<string, unknown>
|
|
|
43
43
|
* ```
|
|
44
44
|
*/
|
|
45
45
|
locals: TLocals;
|
|
46
|
+
/**
|
|
47
|
+
* Parse the request body as JSON.
|
|
48
|
+
*
|
|
49
|
+
* Thin wrapper around `request.json()` with error handling.
|
|
50
|
+
* Throws `BodyParseError` if the body is not valid JSON.
|
|
51
|
+
*
|
|
52
|
+
* @typeParam T — Expected shape of the parsed JSON body
|
|
53
|
+
* @returns The parsed JSON body
|
|
54
|
+
* @throws {BodyParseError} If the body cannot be parsed as JSON
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* app.post("/users", async (ctx) => {
|
|
59
|
+
* const body = await ctx.json<{ name: string }>();
|
|
60
|
+
* return { id: 1, name: body.name };
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
json: <T = unknown>() => Promise<T>;
|
|
65
|
+
/**
|
|
66
|
+
* Get the request body as a string.
|
|
67
|
+
*
|
|
68
|
+
* Thin wrapper around `request.text()`.
|
|
69
|
+
*
|
|
70
|
+
* @returns The request body as text
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* app.post("/echo", async (ctx) => {
|
|
75
|
+
* const text = await ctx.text();
|
|
76
|
+
* return { echo: text };
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
text: () => Promise<string>;
|
|
81
|
+
/**
|
|
82
|
+
* Parse the request body as FormData.
|
|
83
|
+
*
|
|
84
|
+
* Thin wrapper around `request.formData()` with error handling.
|
|
85
|
+
* Throws `BodyParseError` if the body cannot be parsed as form data.
|
|
86
|
+
*
|
|
87
|
+
* @returns The parsed FormData
|
|
88
|
+
* @throws {BodyParseError} If the body cannot be parsed as form data
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* app.post("/upload", async (ctx) => {
|
|
93
|
+
* const form = await ctx.formData();
|
|
94
|
+
* const name = form.get("name");
|
|
95
|
+
* return { name };
|
|
96
|
+
* });
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
formData: () => ReturnType<Request["formData"]>;
|
|
46
100
|
}
|
|
47
101
|
//# sourceMappingURL=requestContext.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"requestContext.d.ts","sourceRoot":"","sources":["../../src/types/requestContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,cAAc,CAC9B,OAAO,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChD,OAAO,SAAS,UAAU,GAAG,UAAU;IAEvC,sCAAsC;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,MAAM,EAAE,OAAO,CAAC;IAChB,kDAAkD;IAClD,KAAK,EAAE,eAAe,CAAC;IACvB;;;;;;;;;;;;OAYG;IACH,MAAM,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"requestContext.d.ts","sourceRoot":"","sources":["../../src/types/requestContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,cAAc,CAC9B,OAAO,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChD,OAAO,SAAS,UAAU,GAAG,UAAU;IAEvC,sCAAsC;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,MAAM,EAAE,OAAO,CAAC;IAChB,kDAAkD;IAClD,KAAK,EAAE,eAAe,CAAC;IACvB;;;;;;;;;;;;OAYG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;IAEpC;;;;;;;;;;;;;;OAcG;IACH,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAE5B;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,EAAE,MAAM,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;CAChD"}
|