@auth-link/web 0.0.1 → 0.0.2

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/dist/index.js CHANGED
@@ -1694,6 +1694,9 @@ function qrImage(ticket) {
1694
1694
  qr.make();
1695
1695
  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(qr.createSvgTag({ cellSize: 4, margin: 16, scalable: true }))}`;
1696
1696
  }
1697
+ function backendProtocol(url) {
1698
+ return url.protocol === "https:" || url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
1699
+ }
1697
1700
  async function limitedJson(response) {
1698
1701
  const reader = response.body?.getReader();
1699
1702
  if (!reader) fail("INVALID_RESPONSE");
@@ -1728,30 +1731,66 @@ async function limitedJson(response) {
1728
1731
  function createAuthLinkClient(options) {
1729
1732
  if ("apiKey" in options || "secret" in options) fail("SERVER_SECRET_FORBIDDEN");
1730
1733
  const base = options.baseUrl ?? "/api/authlink";
1734
+ const allowedOrigins = new Set(options.allowedBackendOrigins ?? []);
1735
+ for (const origin of allowedOrigins) {
1736
+ let url;
1737
+ try {
1738
+ url = new URL(origin);
1739
+ } catch {
1740
+ return fail("INVALID_BACKEND_ORIGIN");
1741
+ }
1742
+ if (url.origin !== origin || url.hostname.includes("*") || !backendProtocol(url)) fail("INVALID_BACKEND_ORIGIN");
1743
+ }
1744
+ const credentials = options.credentials ?? (options.accessToken ? "omit" : "same-origin");
1745
+ if (!["omit", "same-origin", "include"].includes(credentials)) fail("INVALID_CREDENTIALS");
1746
+ const csrfHeaderName = options.csrfHeaderName ?? "X-CSRF-Token";
1747
+ const reserved = /* @__PURE__ */ new Set(["accept", "content-type", "authorization", "idempotency-key"]);
1748
+ try {
1749
+ new Headers([[csrfHeaderName, "test"]]);
1750
+ } catch {
1751
+ return fail("INVALID_CSRF_HEADER");
1752
+ }
1753
+ if (reserved.has(csrfHeaderName.toLowerCase())) fail("INVALID_CSRF_HEADER");
1754
+ reserved.add(csrfHeaderName.toLowerCase());
1731
1755
  const call = async (path, body, signal, key) => {
1732
1756
  if (typeof window === "undefined") fail("BROWSER_REQUIRED");
1733
- const url = new URL(base.replace(/\/$/, "") + path, window.location.href);
1734
- if (url.origin !== window.location.origin || url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) fail("CMS_ORIGIN_REQUIRED");
1735
- const csrf = await options.csrfToken();
1736
- if (typeof csrf !== "string" || !csrf || csrf.length > 1024 || /[\r\n]/.test(csrf)) fail("CSRF_REQUIRED");
1757
+ let url;
1758
+ try {
1759
+ url = new URL(base.replace(/\/+$/, "") + path, window.location.href);
1760
+ } catch {
1761
+ return fail("INVALID_BASE_URL");
1762
+ }
1763
+ if (url.username || url.password || url.search || url.hash || !backendProtocol(url)) fail("INVALID_BASE_URL");
1764
+ if (url.origin !== window.location.origin && !allowedOrigins.has(url.origin)) fail("BACKEND_ORIGIN_NOT_ALLOWED");
1737
1765
  signal?.throwIfAborted();
1738
1766
  const controller = new AbortController();
1739
1767
  const cancel = () => controller.abort(signal?.reason);
1740
1768
  signal?.addEventListener("abort", cancel, { once: true });
1741
1769
  const timeout = setTimeout(() => controller.abort(), 1e4);
1742
1770
  try {
1771
+ const headers = new Headers(typeof options.headers === "function" ? await abortable(Promise.resolve().then(options.headers), controller.signal) : options.headers);
1772
+ for (const name of reserved) if (headers.has(name)) fail("RESERVED_HEADER");
1773
+ headers.set("Accept", "application/json");
1774
+ headers.set("Content-Type", "application/json");
1775
+ if (key) headers.set("Idempotency-Key", key);
1776
+ if (options.csrfToken) {
1777
+ const csrf = await abortable(Promise.resolve().then(options.csrfToken), controller.signal);
1778
+ if (typeof csrf !== "string" || !csrf.trim() || csrf.length > 1024 || /[\r\n]/.test(csrf)) fail("CSRF_REQUIRED");
1779
+ headers.set(csrfHeaderName, csrf);
1780
+ } else if (!options.accessToken || credentials !== "omit") fail("CSRF_REQUIRED");
1781
+ if (options.accessToken) {
1782
+ const accessToken = await abortable(Promise.resolve().then(options.accessToken), controller.signal);
1783
+ if (typeof accessToken !== "string" || !accessToken || /\s/.test(accessToken)) fail("ACCESS_TOKEN_REQUIRED");
1784
+ headers.set("Authorization", `Bearer ${accessToken}`);
1785
+ }
1786
+ controller.signal.throwIfAborted();
1743
1787
  const response = await (options.fetch ?? globalThis.fetch)(url, {
1744
1788
  method: "POST",
1745
- credentials: "same-origin",
1789
+ credentials,
1746
1790
  redirect: "error",
1747
1791
  cache: "no-store",
1748
1792
  signal: controller.signal,
1749
- headers: {
1750
- Accept: "application/json",
1751
- "Content-Type": "application/json",
1752
- "X-CSRF-Token": csrf,
1753
- ...key ? { "Idempotency-Key": key } : {}
1754
- },
1793
+ headers,
1755
1794
  body: JSON.stringify(body)
1756
1795
  });
1757
1796
  if (!response.ok) fail(response.status === 401 || response.status === 403 ? "CMS_UNAUTHORIZED" : "CMS_REQUEST_FAILED");