@b9g/router 0.1.10 → 0.2.0-beta.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.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Standard middleware utilities for HTTP routing
3
+ */
4
+ import type { FunctionMiddleware } from "./index.js";
5
+ /**
6
+ * Mode for trailing slash normalization
7
+ */
8
+ export type TrailingSlashMode = "strip" | "add";
9
+ /**
10
+ * Middleware that normalizes trailing slashes via 301 redirect
11
+ *
12
+ * @param mode - "strip" removes trailing slash, "add" adds trailing slash
13
+ * @returns Function middleware that redirects non-canonical URLs
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import {Router} from "@b9g/router";
18
+ * import {trailingSlash} from "@b9g/router/middleware";
19
+ *
20
+ * const router = new Router();
21
+ * router.use(trailingSlash("strip")); // Redirect /path/ → /path
22
+ *
23
+ * // Can also be scoped to specific paths
24
+ * router.use("/api", trailingSlash("strip"));
25
+ * ```
26
+ */
27
+ export declare function trailingSlash(mode: TrailingSlashMode): FunctionMiddleware;
28
+ /**
29
+ * CORS configuration options
30
+ */
31
+ export interface CORSOptions {
32
+ /**
33
+ * Allowed origins. Can be:
34
+ * - "*" for any origin (not recommended with credentials)
35
+ * - A specific origin string: "https://example.com"
36
+ * - An array of origins: ["https://example.com", "https://app.example.com"]
37
+ * - A function that receives the origin and returns true/false
38
+ *
39
+ * @default "*"
40
+ */
41
+ origin?: string | string[] | ((origin: string) => boolean);
42
+ /**
43
+ * Allowed HTTP methods
44
+ * @default ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]
45
+ */
46
+ methods?: string[];
47
+ /**
48
+ * Allowed request headers
49
+ * @default ["Content-Type", "Authorization"]
50
+ */
51
+ allowedHeaders?: string[];
52
+ /**
53
+ * Headers exposed to the browser
54
+ */
55
+ exposedHeaders?: string[];
56
+ /**
57
+ * Whether to include credentials (cookies, authorization headers)
58
+ * Note: Cannot be used with origin: "*"
59
+ * @default false
60
+ */
61
+ credentials?: boolean;
62
+ /**
63
+ * Max age for preflight cache in seconds
64
+ * @default 86400 (24 hours)
65
+ */
66
+ maxAge?: number;
67
+ }
68
+ /**
69
+ * CORS middleware factory
70
+ *
71
+ * Handles Cross-Origin Resource Sharing headers and preflight requests.
72
+ * Use as generator middleware to add CORS headers to all responses.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * import {Router} from "@b9g/router";
77
+ * import {cors} from "@b9g/router/middleware";
78
+ *
79
+ * const router = new Router();
80
+ *
81
+ * // Allow all origins
82
+ * router.use(cors());
83
+ *
84
+ * // Allow specific origin with credentials
85
+ * router.use(cors({
86
+ * origin: "https://myapp.com",
87
+ * credentials: true
88
+ * }));
89
+ *
90
+ * // Allow multiple origins
91
+ * router.use(cors({
92
+ * origin: ["https://app.example.com", "https://admin.example.com"]
93
+ * }));
94
+ *
95
+ * // Dynamic origin validation
96
+ * router.use(cors({
97
+ * origin: (origin) => origin.endsWith(".example.com")
98
+ * }));
99
+ * ```
100
+ */
101
+ export declare function cors(options?: CORSOptions): (request: Request, _context: any) => AsyncGenerator<Request, Response | undefined, Response>;
@@ -0,0 +1,107 @@
1
+ /// <reference types="./middleware.d.ts" />
2
+ // src/middleware.ts
3
+ function trailingSlash(mode) {
4
+ return (req) => {
5
+ const url = new URL(req.url);
6
+ const pathname = url.pathname;
7
+ if (pathname === "/") return;
8
+ let newPathname = null;
9
+ if (mode === "strip" && pathname.endsWith("/")) {
10
+ newPathname = pathname.slice(0, -1);
11
+ } else if (mode === "add" && !pathname.endsWith("/")) {
12
+ newPathname = pathname + "/";
13
+ }
14
+ if (newPathname) {
15
+ url.pathname = newPathname;
16
+ return new Response(null, {
17
+ status: 301,
18
+ headers: { Location: url.toString() }
19
+ });
20
+ }
21
+ };
22
+ }
23
+ var DEFAULT_CORS_METHODS = ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"];
24
+ var DEFAULT_CORS_HEADERS = ["Content-Type", "Authorization"];
25
+ var DEFAULT_CORS_MAX_AGE = 86400;
26
+ function getAllowedOrigin(config, requestOrigin) {
27
+ if (config === "*") {
28
+ return "*";
29
+ }
30
+ if (typeof config === "string") {
31
+ return config === requestOrigin ? config : null;
32
+ }
33
+ if (Array.isArray(config)) {
34
+ return config.includes(requestOrigin) ? requestOrigin : null;
35
+ }
36
+ if (typeof config === "function") {
37
+ return config(requestOrigin) ? requestOrigin : null;
38
+ }
39
+ return null;
40
+ }
41
+ function cors(options = {}) {
42
+ const {
43
+ origin = "*",
44
+ methods = DEFAULT_CORS_METHODS,
45
+ allowedHeaders = DEFAULT_CORS_HEADERS,
46
+ exposedHeaders,
47
+ credentials = false,
48
+ maxAge = DEFAULT_CORS_MAX_AGE
49
+ } = options;
50
+ if (credentials && origin === "*") {
51
+ throw new Error(
52
+ 'CORS: credentials cannot be used with origin: "*". Specify allowed origins explicitly.'
53
+ );
54
+ }
55
+ return async function* (request, _context) {
56
+ const requestOrigin = request.headers.get("Origin");
57
+ if (!requestOrigin) {
58
+ const response2 = yield request;
59
+ return response2;
60
+ }
61
+ const allowedOrigin = getAllowedOrigin(origin, requestOrigin);
62
+ if (!allowedOrigin) {
63
+ if (request.method === "OPTIONS") {
64
+ return new Response(null, { status: 403 });
65
+ }
66
+ const response2 = yield request;
67
+ return response2;
68
+ }
69
+ if (request.method === "OPTIONS") {
70
+ const headers = new Headers();
71
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
72
+ headers.set("Access-Control-Allow-Methods", methods.join(", "));
73
+ headers.set("Access-Control-Allow-Headers", allowedHeaders.join(", "));
74
+ headers.set("Access-Control-Max-Age", String(maxAge));
75
+ if (credentials) {
76
+ headers.set("Access-Control-Allow-Credentials", "true");
77
+ }
78
+ if (exposedHeaders?.length) {
79
+ headers.set("Access-Control-Expose-Headers", exposedHeaders.join(", "));
80
+ }
81
+ headers.set("Vary", "Origin");
82
+ return new Response(null, { status: 204, headers });
83
+ }
84
+ const response = yield request;
85
+ const newHeaders = new Headers(response.headers);
86
+ newHeaders.set("Access-Control-Allow-Origin", allowedOrigin);
87
+ if (credentials) {
88
+ newHeaders.set("Access-Control-Allow-Credentials", "true");
89
+ }
90
+ if (exposedHeaders?.length) {
91
+ newHeaders.set(
92
+ "Access-Control-Expose-Headers",
93
+ exposedHeaders.join(", ")
94
+ );
95
+ }
96
+ newHeaders.set("Vary", "Origin");
97
+ return new Response(response.body, {
98
+ status: response.status,
99
+ statusText: response.statusText,
100
+ headers: newHeaders
101
+ });
102
+ };
103
+ }
104
+ export {
105
+ cors,
106
+ trailingSlash
107
+ };