1 |
|
2 | import type { Temporal } from 'temporal-spec';
|
3 | import type { SetRequired } from 'type-fest';
|
4 | import type { Awaitable } from "./utils/common-types.js";
|
5 | import type { Context } from "./index.js";
|
6 |
|
7 | export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
8 |
|
9 | export const ORIGIN = 'Origin';
|
10 | export const REQUEST_METHOD = 'Access-Control-Request-Method';
|
11 | export const REQUEST_HEADERS = 'Access-Control-Request-Headers';
|
12 | export const ALLOW_ORIGIN = 'Access-Control-Allow-Origin';
|
13 | export const ALLOW_METHODS = 'Access-Control-Allow-Methods';
|
14 | export const ALLOW_HEADERS = 'Access-Control-Allow-Headers';
|
15 | export const ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials';
|
16 | export const VARY = 'VARY';
|
17 |
|
18 | export interface CORSOptions {
|
19 | origin?: string | { origin: string }
|
20 | methods?: Method[],
|
21 | headers?: string[],
|
22 | credentials?: boolean;
|
23 | maxAge?: number | Temporal.Duration;
|
24 | }
|
25 |
|
26 | export type StrictCORSOptions = SetRequired<CORSOptions, 'origin' | 'methods' | 'headers'>;
|
27 |
|
28 | const SECOND = { unit: 'second', relativeTo: '1970-01-01' } as Temporal.DurationTotalOf;
|
29 | const isDuration = (x?: unknown): x is Temporal.Duration => (<any>x)?.[Symbol.toStringTag] === 'Temporal.Duration'
|
30 | const toMaxAge = (x: number | Temporal.Duration) => (isDuration(x) ? x.total(SECOND) : x).toString()
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | export 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 |
|
79 | export { cors as anyCORS }
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | export 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 | }
|