@c9up/aurora 0.1.34 → 0.1.37

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/src/xsrf.ts ADDED
@@ -0,0 +1,97 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Echoing the CSRF cookie back as a header, in one place.
4
+ *
5
+ * The signed double-submit guard on the server reads `XSRF-TOKEN` from the
6
+ * cookie jar and compares it to `X-XSRF-TOKEN` on the request. A browser sends
7
+ * the cookie on its own; the header is the client's half, and it is the half
8
+ * that says "this request came from our own page", because a cross-site caller
9
+ * can cause the cookie to ride along but cannot read it.
10
+ *
11
+ * This lived three times over. `rpc.ts` and `relay.ts` each had a copy, and
12
+ * both described theirs as mirroring `HttpClient.#retrieveXsrfToken` — a method
13
+ * `HttpClient` never had. So the client the docs tell you to submit a form with
14
+ * sent no header at all, and every POST through it was refused the moment an
15
+ * application turned CSRF on. One reader now, used by all three.
16
+ */
17
+
18
+ /** The cookie the server seeds. */
19
+ export const XSRF_COOKIE_NAME = "XSRF-TOKEN";
20
+
21
+ /** The header it is echoed in (the Axios/Angular convention the server reads). */
22
+ export const XSRF_HEADER_NAME = "X-XSRF-TOKEN";
23
+
24
+ /** Per-client switches for the automatic header. */
25
+ export interface XsrfOptions {
26
+ /**
27
+ * Echo the CSRF cookie as a header on same-origin requests. Default `true`.
28
+ * A no-op outside a browser and when the cookie is absent — a bearer-authed
29
+ * API is CSRF-exempt and seeds no cookie, so nothing is sent there either.
30
+ */
31
+ xsrf?: boolean;
32
+ /** Cookie to read the token from. Default `XSRF-TOKEN`. */
33
+ xsrfCookieName?: string;
34
+ /** Header to echo it in. Default `X-XSRF-TOKEN`. */
35
+ xsrfHeaderName?: string;
36
+ }
37
+
38
+ /**
39
+ * Read a cookie's raw value from `document.cookie`.
40
+ *
41
+ * Verbatim, never decoded: the server compares the header to the cookie
42
+ * byte-for-byte. The token is hex + `.` + base64url, so there is nothing a
43
+ * decode could change — but a decode that ever did change something would turn
44
+ * a valid request into a rejected one, silently.
45
+ *
46
+ * Returns `undefined` server-side (no `document`) or when the cookie is absent.
47
+ */
48
+ export function readXsrfCookie(
49
+ name: string = XSRF_COOKIE_NAME,
50
+ ): string | undefined {
51
+ if (typeof document === "undefined") return undefined;
52
+ const prefix = `${name}=`;
53
+ for (const part of document.cookie.split(";")) {
54
+ const trimmed = part.trimStart();
55
+ if (trimmed.startsWith(prefix)) return trimmed.slice(prefix.length);
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ /**
61
+ * Is this URL served by the page's own origin?
62
+ *
63
+ * The question is not "does it match the client's baseURL" — a client whose
64
+ * baseURL IS a third-party API would pass that one. A CSRF token authenticates
65
+ * the page's session; sending it anywhere else hands a working token to whoever
66
+ * runs that host.
67
+ *
68
+ * A relative URL is same-origin by construction. Outside a browser there is no
69
+ * page and no cookie, so the answer is no.
70
+ */
71
+ export function isSameOriginAsPage(url: string): boolean {
72
+ if (typeof window === "undefined") return false;
73
+ if (!/^[a-z][a-z\d+\-.]*:\/\//i.test(url)) return true;
74
+ try {
75
+ return new URL(url).origin === window.location.origin;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * The header to add for `url`, or `undefined` when there is nothing to send.
83
+ *
84
+ * Absent cookie, disabled, cross-origin target, or no browser: nothing. The
85
+ * caller merges the result rather than being handed an empty object, so a call
86
+ * site cannot accidentally overwrite a header it set itself.
87
+ */
88
+ export function xsrfHeaderFor(
89
+ url: string,
90
+ options: XsrfOptions = {},
91
+ ): Record<string, string> | undefined {
92
+ if (options.xsrf === false) return undefined;
93
+ if (!isSameOriginAsPage(url)) return undefined;
94
+ const token = readXsrfCookie(options.xsrfCookieName ?? XSRF_COOKIE_NAME);
95
+ if (token === undefined) return undefined;
96
+ return { [options.xsrfHeaderName ?? XSRF_HEADER_NAME]: token };
97
+ }