@b9g/router 0.1.10 → 0.2.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/README.md +173 -108
- package/package.json +14 -7
- package/src/index.d.ts +63 -93
- package/src/index.js +281 -212
- package/src/middleware.d.ts +101 -0
- package/src/middleware.js +107 -0
|
@@ -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
|
+
};
|