UNPKG

4.41 kBPlain TextView Raw
1// deno-lint-ignore-file no-explicit-any
2import type { Temporal } from 'temporal-spec';
3import type { SetRequired } from 'type-fest';
4import type { Awaitable } from "./utils/common-types.js";
5import type { Context } from "./index.js";
6
7export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
8
9export const ORIGIN = 'Origin';
10export const REQUEST_METHOD = 'Access-Control-Request-Method';
11export const REQUEST_HEADERS = 'Access-Control-Request-Headers';
12export const ALLOW_ORIGIN = 'Access-Control-Allow-Origin';
13export const ALLOW_METHODS = 'Access-Control-Allow-Methods';
14export const ALLOW_HEADERS = 'Access-Control-Allow-Headers';
15export const ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials';
16export const VARY = 'VARY';
17
18export interface CORSOptions {
19 origin?: string | { origin: string }
20 methods?: Method[],
21 headers?: string[],
22 credentials?: boolean;
23 maxAge?: number | Temporal.Duration;
24}
25
26export type StrictCORSOptions = SetRequired<CORSOptions, 'origin' | 'methods' | 'headers'>;
27
28const SECOND = { unit: 'second', relativeTo: '1970-01-01' } as Temporal.DurationTotalOf;
29const isDuration = (x?: unknown): x is Temporal.Duration => (<any>x)?.[Symbol.toStringTag] === 'Temporal.Duration'
30const toMaxAge = (x: number | Temporal.Duration) => (isDuration(x) ? x.total(SECOND) : x).toString()
31
32/**
33 * A CORS middleware that gives clients exactly the permissions they ask for, unless constrained by the definitions in `options`.
34 *
35 * Note that applying this middleware to your routes isn't enough for non-GET requests.
36 * Pre-flight/OPTIONS routes need to be added manually:
37 * ```
38 * router.options('/your/path', anyCORS(), () => noContent())
39 * router.post('/your/path', anyCORS(), (req, {}) => ok())
40 * ```
41 */
42export const cors = (options: CORSOptions = {}) => async <X extends Context>(ax: Awaitable<X>): Promise<X> => {
43 const x = await ax;
44 const req = x.request;
45
46 x.effects.push(res => {
47 const optOrigin = typeof options.origin === 'string'
48 ? new URL(options.origin)
49 : options.origin;
50
51 res.headers.set(ALLOW_ORIGIN, optOrigin?.origin ?? req.headers.get(ORIGIN) ?? '*');
52
53 const requestedMethod = <Method>req.headers.get(REQUEST_METHOD);
54 if (requestedMethod && (options.methods?.includes(requestedMethod) ?? true)) {
55 res.headers.append(ALLOW_METHODS, requestedMethod);
56 }
57
58 const requestedHeaders = new Set(req.headers.get(REQUEST_HEADERS)?.split(',')?.map(h => h.trim()))
59 for (const h of options.headers?.filter(h => requestedHeaders.has(h)) ?? requestedHeaders) {
60 res.headers.append(ALLOW_HEADERS, h);
61 }
62
63 if (options.credentials)
64 res.headers.set(ALLOW_CREDENTIALS, 'true');
65
66 if (options.maxAge)
67 res.headers.set('Access-Control-Max-Age', toMaxAge(options.maxAge))
68
69 if (!options.origin) res.headers.append(VARY, ORIGIN)
70 if (!options.methods) res.headers.append(VARY, REQUEST_METHOD)
71 if (!options.headers) res.headers.append(VARY, REQUEST_HEADERS)
72
73 return res;
74 })
75
76 return x;
77}
78
79export { cors as anyCORS }
80
81/**
82 * A CORS middleware that only grants the permissions defined via `options`.
83 *
84 * Note that applying this middleware to your routes isn't enough for non-GET requests.
85 * Pre-flight/OPTIONS routes need to be added manually:
86 * ```
87 * router.options('/your/path', strictCORS({ ... }), () => noContent())
88 * router.post('/your/path', strictCORS({ ... }), (req, {}) => ok())
89 * ```
90 */
91export const strictCORS = (options: StrictCORSOptions) => async <X extends Context>(ax: Awaitable<X>): Promise<X> => {
92 const x = await ax;
93 const req = x.request;
94
95 x.effects.push(res => {
96 const optOrigin = typeof options.origin === 'string'
97 ? new URL(options.origin)
98 : options.origin;
99
100 res.headers.set(ALLOW_ORIGIN, optOrigin.origin);
101
102 const requestedMethod = <Method>req.headers.get(REQUEST_METHOD);
103 if (requestedMethod && options.methods.includes(requestedMethod)) {
104 for (const m of options.methods) {
105 res.headers.append(ALLOW_METHODS, m);
106 }
107 }
108
109 if (req.headers.get(REQUEST_HEADERS)) {
110 for (const h of options.headers) {
111 res.headers.append(ALLOW_HEADERS, h);
112 }
113 }
114
115 if (options.credentials)
116 res.headers.set(ALLOW_CREDENTIALS, 'true');
117
118 if (options.maxAge)
119 res.headers.set('Access-Control-Max-Age', toMaxAge(options.maxAge))
120
121 return res;
122 })
123
124 return x;
125}