From 9849b9753cbbbe26cd42ceda254a8421a6fb55c8 Mon Sep 17 00:00:00 2001 From: Jan Niklas Marzahl Date: Sat, 1 Jul 2023 14:29:20 +0200 Subject: [PATCH] created a docs website --- docs/website/.gitignore | 2 + docs/website/next.config.js | 6 + docs/website/package-lock.json | 4147 +++++++++++++++++ docs/website/package.json | 23 + docs/website/pages/_meta.json | 5 + docs/website/pages/index.mdx | 18 + docs/website/pages/overview/_meta.json | 5 + docs/website/pages/overview/advanced.mdx | 112 + docs/website/pages/overview/installation.mdx | 85 + docs/website/pages/overview/quickstart.mdx | 171 + docs/website/pages/strategies/_meta.json | 25 + .../website/pages/strategies/auth-socials.mdx | 248 + docs/website/pages/strategies/auth0.mdx | 137 + docs/website/pages/strategies/bitbucket.mdx | 99 + docs/website/pages/strategies/bungie.mdx | 132 + docs/website/pages/strategies/discord.mdx | 167 + docs/website/pages/strategies/email.mdx | 350 ++ docs/website/pages/strategies/form.mdx | 111 + docs/website/pages/strategies/github.mdx | 105 + docs/website/pages/strategies/gitlab.mdx | 99 + .../pages/strategies/google-credential.mdx | 73 + docs/website/pages/strategies/google.mdx | 102 + docs/website/pages/strategies/keycloak.mdx | 109 + docs/website/pages/strategies/linkedin.mdx | 137 + docs/website/pages/strategies/microsoft.mdx | 130 + docs/website/pages/strategies/notion.mdx | 90 + docs/website/pages/strategies/okta.mdx | 189 + docs/website/pages/strategies/otp.mdx | 683 +++ docs/website/pages/strategies/spotify.mdx | 206 + docs/website/pages/strategies/steam.mdx | 143 + docs/website/pages/strategies/twillio.mdx | 225 + docs/website/pages/strategies/twitch.mdx | 115 + docs/website/pages/strategies/twitter.mdx | 137 + docs/website/pages/strategies/web-authn.mdx | 305 ++ docs/website/theme.config.tsx | 25 + 35 files changed, 8716 insertions(+) create mode 100644 docs/website/.gitignore create mode 100644 docs/website/next.config.js create mode 100644 docs/website/package-lock.json create mode 100644 docs/website/package.json create mode 100644 docs/website/pages/_meta.json create mode 100644 docs/website/pages/index.mdx create mode 100644 docs/website/pages/overview/_meta.json create mode 100644 docs/website/pages/overview/advanced.mdx create mode 100644 docs/website/pages/overview/installation.mdx create mode 100644 docs/website/pages/overview/quickstart.mdx create mode 100644 docs/website/pages/strategies/_meta.json create mode 100644 docs/website/pages/strategies/auth-socials.mdx create mode 100644 docs/website/pages/strategies/auth0.mdx create mode 100644 docs/website/pages/strategies/bitbucket.mdx create mode 100644 docs/website/pages/strategies/bungie.mdx create mode 100644 docs/website/pages/strategies/discord.mdx create mode 100644 docs/website/pages/strategies/email.mdx create mode 100644 docs/website/pages/strategies/form.mdx create mode 100644 docs/website/pages/strategies/github.mdx create mode 100644 docs/website/pages/strategies/gitlab.mdx create mode 100644 docs/website/pages/strategies/google-credential.mdx create mode 100644 docs/website/pages/strategies/google.mdx create mode 100644 docs/website/pages/strategies/keycloak.mdx create mode 100644 docs/website/pages/strategies/linkedin.mdx create mode 100644 docs/website/pages/strategies/microsoft.mdx create mode 100644 docs/website/pages/strategies/notion.mdx create mode 100644 docs/website/pages/strategies/okta.mdx create mode 100644 docs/website/pages/strategies/otp.mdx create mode 100644 docs/website/pages/strategies/spotify.mdx create mode 100644 docs/website/pages/strategies/steam.mdx create mode 100644 docs/website/pages/strategies/twillio.mdx create mode 100644 docs/website/pages/strategies/twitch.mdx create mode 100644 docs/website/pages/strategies/twitter.mdx create mode 100644 docs/website/pages/strategies/web-authn.mdx create mode 100644 docs/website/theme.config.tsx diff --git a/docs/website/.gitignore b/docs/website/.gitignore new file mode 100644 index 0000000..f74c781 --- /dev/null +++ b/docs/website/.gitignore @@ -0,0 +1,2 @@ +.next +node_modules diff --git a/docs/website/next.config.js b/docs/website/next.config.js new file mode 100644 index 0000000..2b9220a --- /dev/null +++ b/docs/website/next.config.js @@ -0,0 +1,6 @@ +const withNextra = require("nextra")({ + theme: "nextra-theme-docs", + themeConfig: "./theme.config.tsx", +}); + +module.exports = withNextra(); diff --git a/docs/website/package-lock.json b/docs/website/package-lock.json new file mode 100644 index 0000000..791252f --- /dev/null +++ b/docs/website/package-lock.json @@ -0,0 +1,4147 @@ +{ + "name": "website-docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "website-docs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "next": "^13.4.7", + "nextra": "^2.8.0", + "nextra-theme-docs": "^2.8.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz", + "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==" + }, + "node_modules/@headlessui/react": { + "version": "1.7.15", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.15.tgz", + "integrity": "sha512-OTO0XtoRQ6JPB1cKNFYBZv2Q0JMqMGNhYP1CjPvcJvjz8YGokz8oAj89HIYZGN0gZzn/4kk9iUpmMF4Q21Gsqw==", + "dependencies": { + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz", + "integrity": "sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/mdx": "^2.0.0", + "estree-util-build-jsx": "^2.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "estree-util-to-js": "^1.1.0", + "estree-walker": "^3.0.0", + "hast-util-to-estree": "^2.0.0", + "markdown-extensions": "^1.0.0", + "periscopic": "^3.0.0", + "remark-mdx": "^2.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "unified": "^10.0.0", + "unist-util-position-from-estree": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-2.3.0.tgz", + "integrity": "sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==", + "dependencies": { + "@types/mdx": "^2.0.0", + "@types/react": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@napi-rs/simple-git": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git/-/simple-git-0.1.8.tgz", + "integrity": "sha512-BvOMdkkofTz6lEE35itJ/laUokPhr/5ToMGlOH25YnhLD2yN1KpRAT4blW9tT8281/1aZjW3xyi73bs//IrDKA==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/simple-git-android-arm-eabi": "0.1.8", + "@napi-rs/simple-git-android-arm64": "0.1.8", + "@napi-rs/simple-git-darwin-arm64": "0.1.8", + "@napi-rs/simple-git-darwin-x64": "0.1.8", + "@napi-rs/simple-git-linux-arm-gnueabihf": "0.1.8", + "@napi-rs/simple-git-linux-arm64-gnu": "0.1.8", + "@napi-rs/simple-git-linux-arm64-musl": "0.1.8", + "@napi-rs/simple-git-linux-x64-gnu": "0.1.8", + "@napi-rs/simple-git-linux-x64-musl": "0.1.8", + "@napi-rs/simple-git-win32-arm64-msvc": "0.1.8", + "@napi-rs/simple-git-win32-x64-msvc": "0.1.8" + } + }, + "node_modules/@napi-rs/simple-git-android-arm-eabi": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm-eabi/-/simple-git-android-arm-eabi-0.1.8.tgz", + "integrity": "sha512-JJCejHBB1G6O8nxjQLT4quWCcvLpC3oRdJJ9G3MFYSCoYS8i1bWCWeU+K7Br+xT+D6s1t9q8kNJAwJv9Ygpi0g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-android-arm64": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-android-arm64/-/simple-git-android-arm64-0.1.8.tgz", + "integrity": "sha512-mraHzwWBw3tdRetNOS5KnFSjvdAbNBnjFLA8I4PwTCPJj3Q4txrigcPp2d59cJ0TC51xpnPXnZjYdNwwSI9g6g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-darwin-arm64": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-arm64/-/simple-git-darwin-arm64-0.1.8.tgz", + "integrity": "sha512-ufy/36eI/j4UskEuvqSH7uXtp3oXeLDmjQCfKJz3u5Vx98KmOMKrqAm2H81AB2WOtCo5mqS6PbBeUXR8BJX8lQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-darwin-x64": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-darwin-x64/-/simple-git-darwin-x64-0.1.8.tgz", + "integrity": "sha512-Vb21U+v3tPJNl+8JtIHHT8HGe6WZ8o1Tq3f6p+Jx9Cz71zEbcIiB9FCEMY1knS/jwQEOuhhlI9Qk7d4HY+rprA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm-gnueabihf": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm-gnueabihf/-/simple-git-linux-arm-gnueabihf-0.1.8.tgz", + "integrity": "sha512-6BPTJ7CzpSm2t54mRLVaUr3S7ORJfVJoCk2rQ8v8oDg0XAMKvmQQxOsAgqKBo9gYNHJnqrOx3AEuEgvB586BuQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm64-gnu": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-gnu/-/simple-git-linux-arm64-gnu-0.1.8.tgz", + "integrity": "sha512-qfESqUCAA/XoQpRXHptSQ8gIFnETCQt1zY9VOkplx6tgYk9PCeaX4B1Xuzrh3eZamSCMJFn+1YB9Ut8NwyGgAA==", + "cpu": [ + "arm64" + ], + "hasInstallScript": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-arm64-musl": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-arm64-musl/-/simple-git-linux-arm64-musl-0.1.8.tgz", + "integrity": "sha512-G80BQPpaRmQpn8dJGHp4I2/YVhWDUNJwcCrJAtAdbKFDCMyCHJBln2ERL/+IEUlIAT05zK/c1Z5WEprvXEdXow==", + "cpu": [ + "arm64" + ], + "hasInstallScript": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-x64-gnu": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-gnu/-/simple-git-linux-x64-gnu-0.1.8.tgz", + "integrity": "sha512-NI6o1sZYEf6vPtNWJAm9w8BxJt+LlSFW0liSjYe3lc3e4dhMfV240f0ALeqlwdIldRPaDFwZSJX5/QbS7nMzhw==", + "cpu": [ + "x64" + ], + "hasInstallScript": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-linux-x64-musl": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.8.tgz", + "integrity": "sha512-wljGAEOW41er45VTiU8kXJmO480pQKzsgRCvPlJJSCaEVBbmo6XXbFIXnZy1a2J3Zyy2IOsRB4PVkUZaNuPkZQ==", + "cpu": [ + "x64" + ], + "hasInstallScript": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-arm64-msvc": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-arm64-msvc/-/simple-git-win32-arm64-msvc-0.1.8.tgz", + "integrity": "sha512-QuV4QILyKPfbWHoQKrhXqjiCClx0SxbCTVogkR89BwivekqJMd9UlMxZdoCmwLWutRx4z9KmzQqokvYI5QeepA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/simple-git-win32-x64-msvc": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-win32-x64-msvc/-/simple-git-win32-x64-msvc-0.1.8.tgz", + "integrity": "sha512-UzNS4JtjhZhZ5hRLq7BIUq+4JOwt1ThIKv11CsF1ag2l99f0123XvfEpjczKTaa94nHtjXYc2Mv9TjccBqYOew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/env": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.7.tgz", + "integrity": "sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw==" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.7.tgz", + "integrity": "sha512-VZTxPv1b59KGiv/pZHTO5Gbsdeoxcj2rU2cqJu03btMhHpn3vwzEK0gUSVC/XW96aeGO67X+cMahhwHzef24/w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.7.tgz", + "integrity": "sha512-gO2bw+2Ymmga+QYujjvDz9955xvYGrWofmxTq7m70b9pDPvl7aDFABJOZ2a8SRCuSNB5mXU8eTOmVVwyp/nAew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.7.tgz", + "integrity": "sha512-6cqp3vf1eHxjIDhEOc7Mh/s8z1cwc/l5B6ZNkOofmZVyu1zsbEM5Hmx64s12Rd9AYgGoiCz4OJ4M/oRnkE16/Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.7.tgz", + "integrity": "sha512-T1kD2FWOEy5WPidOn1si0rYmWORNch4a/NR52Ghyp4q7KyxOCuiOfZzyhVC5tsLIBDH3+cNdB5DkD9afpNDaOw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.7.tgz", + "integrity": "sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.7.tgz", + "integrity": "sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.7.tgz", + "integrity": "sha512-NPnmnV+vEIxnu6SUvjnuaWRglZzw4ox5n/MQTxeUhb5iwVWFedolPFebMNwgrWu4AELwvTdGtWjqof53AiWHcw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.7.tgz", + "integrity": "sha512-6Hxijm6/a8XqLQpOOf/XuwWRhcuc/g4rBB2oxjgCMuV9Xlr2bLs5+lXyh8w9YbAUMYR3iC9mgOlXbHa79elmXw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.7.tgz", + "integrity": "sha512-sW9Yt36Db1nXJL+mTr2Wo0y+VkPWeYhygvcHj1FF0srVtV+VoDjxleKtny21QHaG05zdeZnw2fCtf2+dEqgwqA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", + "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@theguild/remark-mermaid": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.3.tgz", + "integrity": "sha512-fccVR6o4UPUztrBjdUhM4ahwx+X7YHhoxsUoXv2vI07vz4dq+I03Ot0SjuZzDA/H7engxcb8ZxzCUEkZgGr/2g==", + "dependencies": { + "mermaid": "^10.2.2", + "unist-util-visit": "^4.1.2" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/@types/acorn": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", + "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.0.tgz", + "integrity": "sha512-3qvGd0z8F2ENTGr/GG1yViqfiKmRfrXVx5sJyHGFu3z7m5g5utCQtGp/g29JnjflhtQJBv1WDQukHiT58xPcYQ==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", + "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==" + }, + "node_modules/@types/katex": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.14.0.tgz", + "integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==" + }, + "node_modules/@types/mdast": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz", + "integrity": "sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.5.tgz", + "integrity": "sha512-76CqzuD6Q7LC+AtbPqrvD9AqsN0k8bsYo2bM2J8pmNldP1aIPAbzUQ7QbobyXL4eLr1wK5x8FZFe8eF/ubRuBg==" + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "node_modules/@types/react": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz", + "integrity": "sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + }, + "node_modules/@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + }, + "node_modules/acorn": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz", + "integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==" + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/arg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-1.0.0.tgz", + "integrity": "sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/astring": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz", + "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001509", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001509.tgz", + "integrity": "sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dependencies": { + "ansi-styles": "^3.1.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clipboardy": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.2.tgz", + "integrity": "sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw==", + "dependencies": { + "arch": "^2.1.0", + "execa": "^0.8.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz", + "integrity": "sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A==" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/cytoscape": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.25.0.tgz", + "integrity": "sha512-7MW3Iz57mCUo6JQCho6CmPBCbTlJr7LzyEtIkutG255HLVd4XuBg2I9BkTZLI/e4HoaOB/BiAzXuQybQ95+r9Q==", + "dependencies": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==" + }, + "node_modules/d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", + "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dompurify": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz", + "integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ==" + }, + "node_modules/elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz", + "integrity": "sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-2.2.2.tgz", + "integrity": "sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz", + "integrity": "sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz", + "integrity": "sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-1.3.0.tgz", + "integrity": "sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==", + "dependencies": { + "is-plain-obj": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/estree-util-visit": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-1.2.1.tgz", + "integrity": "sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", + "integrity": "sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==", + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/flexsearch": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.7.31.tgz", + "integrity": "sha512-XGozTsMPYkm+6b5QL3Z9wQcJjNYxp0CYn3U1gO7dwD6PAqU1SVWZxI9CCg3z+ml3YfqdPnrBehaBrnH2AGKbNA==" + }, + "node_modules/focus-visible": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/focus-visible/-/focus-visible-5.2.0.tgz", + "integrity": "sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==" + }, + "node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/git-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", + "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", + "dependencies": { + "is-ssh": "^1.4.0", + "parse-url": "^8.1.0" + } + }, + "node_modules/git-url-parse": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz", + "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==", + "dependencies": { + "git-up": "^7.0.0" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-obj": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hash-obj/-/hash-obj-4.0.0.tgz", + "integrity": "sha512-FwO1BUVWkyHasWDW4S8o0ssQXjvyghLV2rfVhnN36b2bbcj45eGiuzdn9XOvOpjV3TKQD7Gm2BWNXdE9V4KKYg==", + "dependencies": { + "is-obj": "^3.0.0", + "sort-keys": "^5.0.0", + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hast-util-from-dom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-4.2.0.tgz", + "integrity": "sha512-t1RJW/OpJbCAJQeKi3Qrj1cAOLA0+av/iPFori112+0X7R3wng+jxLA+kXec8K4szqPRGI8vPxbbpEYvvpwaeQ==", + "dependencies": { + "hastscript": "^7.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-1.0.2.tgz", + "integrity": "sha512-LhrTA2gfCbLOGJq2u/asp4kwuG0y6NhWTXiPKP+n0qNukKy7hc10whqqCFfyvIA1Q5U5d0sp9HhNim9gglEH4A==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-from-parse5": "^7.0.0", + "parse5": "^7.0.0", + "vfile": "^5.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-1.0.0.tgz", + "integrity": "sha512-Yu480AKeOEN/+l5LA674a+7BmIvtDj24GvOt7MtQWuhzUwlaaRWdEPXAh3Qm5vhuthpAipFb2vTetKXWOjmTvw==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-from-dom": "^4.0.0", + "hast-util-from-html": "^1.0.0", + "unist-util-remove-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz", + "integrity": "sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz", + "integrity": "sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "estree-util-attach-comments": "^2.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "mdast-util-mdx-expression": "^1.0.0", + "mdast-util-mdxjs-esm": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.1", + "unist-util-position": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz", + "integrity": "sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "unist-util-find-after": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/intersection-observer": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz", + "integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-reference": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.1.tgz", + "integrity": "sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-ssh": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", + "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "dependencies": { + "protocols": "^2.0.1" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, + "node_modules/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-ftuDnJbcbOckGY11OO+zg3OofESlbR5DRl2cmN8HeWeeFIV7wTXvAOx8kEjZjobhA+9wh2fbKeO6cdcA9Mnovg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/khroma": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", + "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/markdown-extensions": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", + "integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, + "node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "dependencies": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-2.0.2.tgz", + "integrity": "sha512-8gmkKVp9v6+Tgjtq6SYx9kGPpTf6FVYRa53/DLh479aldR9AyP48qeVOgNZ5X7QUK7nOy4yw7vg6mbiGcs9jWQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz", + "integrity": "sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==", + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-mdx-expression": "^1.0.0", + "mdast-util-mdx-jsx": "^2.0.0", + "mdast-util-mdxjs-esm": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz", + "integrity": "sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz", + "integrity": "sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "mdast-util-from-markdown": "^1.1.0", + "mdast-util-to-markdown": "^1.3.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-remove-position": "^4.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz", + "integrity": "sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mermaid": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.2.4.tgz", + "integrity": "sha512-zHGjEI7lBvWZX+PQYmlhSA2p40OzW6QbGodTCSzDeVpqaTnyAC+2sRGqrpXO+uQk3CnoeClHQPraQUMStdqy2g==", + "dependencies": { + "@braintree/sanitize-url": "^6.0.2", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.4.0", + "dagre-d3-es": "7.0.10", + "dayjs": "^1.11.7", + "dompurify": "3.0.3", + "elkjs": "^0.8.2", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^1.3.0", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.3", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-2.1.2.tgz", + "integrity": "sha512-es0CcOV89VNS9wFmyn+wyFTKweXGW4CEvdaAca6SWRWPyYCbBisnjaHLjWO4Nszuiud84jCpkHsqAJoa768Pvg==", + "dependencies": { + "@types/katex": "^0.16.0", + "katex": "^0.16.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math/node_modules/@types/katex": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.0.tgz", + "integrity": "sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw==" + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz", + "integrity": "sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/estree": "^1.0.0", + "micromark-factory-mdx-expression": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz", + "integrity": "sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==", + "dependencies": { + "@types/acorn": "^4.0.0", + "@types/estree": "^1.0.0", + "estree-util-is-identifier-name": "^2.0.0", + "micromark-factory-mdx-expression": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz", + "integrity": "sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz", + "integrity": "sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^1.0.0", + "micromark-extension-mdx-jsx": "^1.0.0", + "micromark-extension-mdx-md": "^1.0.0", + "micromark-extension-mdxjs-esm": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz", + "integrity": "sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==", + "dependencies": { + "@types/estree": "^1.0.0", + "micromark-core-commonmark": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-position-from-estree": "^1.1.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz", + "integrity": "sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/estree": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-events-to-acorn": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-position-from-estree": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz", + "integrity": "sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/acorn": "^4.0.0", + "@types/estree": "^1.0.0", + "@types/unist": "^2.0.0", + "estree-util-visit": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0", + "vfile-message": "^3.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/next/-/next-13.4.7.tgz", + "integrity": "sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==", + "dependencies": { + "@next/env": "13.4.7", + "@swc/helpers": "0.5.1", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.14", + "styled-jsx": "5.1.1", + "watchpack": "2.4.0", + "zod": "3.21.4" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=16.8.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "13.4.7", + "@next/swc-darwin-x64": "13.4.7", + "@next/swc-linux-arm64-gnu": "13.4.7", + "@next/swc-linux-arm64-musl": "13.4.7", + "@next/swc-linux-x64-gnu": "13.4.7", + "@next/swc-linux-x64-musl": "13.4.7", + "@next/swc-win32-arm64-msvc": "13.4.7", + "@next/swc-win32-ia32-msvc": "13.4.7", + "@next/swc-win32-x64-msvc": "13.4.7" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "fibers": ">= 3.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "fibers": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-mdx-remote": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-4.4.1.tgz", + "integrity": "sha512-1BvyXaIou6xy3XoNF4yaMZUCb6vD2GTAa5ciOa6WoO+gAUTYsb1K4rI/HSC2ogAWLrb/7VSV52skz07vOzmqIQ==", + "dependencies": { + "@mdx-js/mdx": "^2.2.1", + "@mdx-js/react": "^2.2.1", + "vfile": "^5.3.0", + "vfile-matter": "^3.0.1" + }, + "engines": { + "node": ">=14", + "npm": ">=7" + }, + "peerDependencies": { + "react": ">=16.x <=18.x", + "react-dom": ">=16.x <=18.x" + } + }, + "node_modules/next-seo": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/next-seo/-/next-seo-6.1.0.tgz", + "integrity": "sha512-iMBpFoJsR5zWhguHJvsoBDxDSmdYTHtnVPB1ij+CD0NReQCP78ZxxbdL9qkKIf4oEuZEqZkrjAQLB0bkII7RYA==", + "peerDependencies": { + "next": "^8.1.1-canary.54 || >=9.0.0", + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/next-themes": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", + "integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==", + "peerDependencies": { + "next": "*", + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nextra": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nextra/-/nextra-2.8.0.tgz", + "integrity": "sha512-WyRNzw1IM/eF3M1H3LSsbZH97QsYYgj8upjx0f8hY6GspmPyPRAvBBscmXRt+7vye2oIYjfVwSiD1rj9amqq9Q==", + "dependencies": { + "@mdx-js/mdx": "^2.3.0", + "@mdx-js/react": "^2.3.0", + "@napi-rs/simple-git": "^0.1.8", + "@theguild/remark-mermaid": "^0.0.3", + "clsx": "^1.2.1", + "github-slugger": "^2.0.0", + "graceful-fs": "^4.2.11", + "gray-matter": "^4.0.3", + "katex": "^0.16.7", + "lodash.get": "^4.4.2", + "next-mdx-remote": "^4.2.1", + "p-limit": "^3.1.0", + "rehype-katex": "^6.0.3", + "rehype-pretty-code": "0.9.9", + "remark-gfm": "^3.0.1", + "remark-math": "^5.1.1", + "remark-reading-time": "^2.0.1", + "shiki": "^0.14.2", + "slash": "^3.0.0", + "title": "^3.5.3", + "unist-util-remove": "^3.1.1", + "unist-util-visit": "^4.1.1", + "zod": "^3.20.2" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "next": ">=9.5.3", + "react": ">=16.13.1", + "react-dom": ">=16.13.1" + } + }, + "node_modules/nextra-theme-docs": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nextra-theme-docs/-/nextra-theme-docs-2.8.0.tgz", + "integrity": "sha512-JoSAILDVp0GxeVWWZBFGoRijE2RcjZcXrMa47Fssi254T5wF+gm0HvEOSwrTaKaPaUL+IfshAiKglvXNKGzbNw==", + "dependencies": { + "@headlessui/react": "^1.7.10", + "@popperjs/core": "^2.11.6", + "clsx": "^1.2.1", + "flexsearch": "^0.7.21", + "focus-visible": "^5.2.0", + "git-url-parse": "^13.1.0", + "intersection-observer": "^0.12.2", + "match-sorter": "^6.3.1", + "next-seo": "^6.0.0", + "next-themes": "^0.2.1", + "scroll-into-view-if-needed": "^3.0.0", + "zod": "^3.20.2" + }, + "peerDependencies": { + "next": ">=9.5.3", + "nextra": "2.8.0", + "react": ">=16.13.1", + "react-dom": ">=16.13.1" + } + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" + }, + "node_modules/parse-path": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", + "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", + "dependencies": { + "protocols": "^2.0.0" + } + }, + "node_modules/parse-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", + "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", + "dependencies": { + "parse-path": "^7.0.0" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz", + "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/protocols": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==" + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/reading-time": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", + "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==" + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/rehype-katex": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-6.0.3.tgz", + "integrity": "sha512-ByZlRwRUcWegNbF70CVRm2h/7xy7jQ3R9LaY4VVSvjnoVWwWVhNL60DiZsBpC5tSzYQOCvDbzncIpIjPZWodZA==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/katex": "^0.14.0", + "hast-util-from-html-isomorphic": "^1.0.0", + "hast-util-to-text": "^3.1.0", + "katex": "^0.16.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-pretty-code": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.9.9.tgz", + "integrity": "sha512-mlU2Qgupn9MMK31CTmWk0Ie5Vp0od+jh2vCkGDBMlPAMeAvYASn6Ois6xRmosutMT4yH/COc3R4r/PELpuUoWg==", + "dependencies": { + "@types/hast": "^2.0.0", + "hash-obj": "^4.0.0", + "parse-numeric-range": "^1.3.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "shiki": "*" + } + }, + "node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-5.1.1.tgz", + "integrity": "sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-math": "^2.0.0", + "micromark-extension-math": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-2.3.0.tgz", + "integrity": "sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==", + "dependencies": { + "mdast-util-mdx": "^2.0.0", + "micromark-extension-mdxjs": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-reading-time": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/remark-reading-time/-/remark-reading-time-2.0.1.tgz", + "integrity": "sha512-fy4BKy9SRhtYbEHvp6AItbRTnrhiDGbqLQTSYVbQPGuRCncU1ubSsh9p/W5QZSxtYcUXv8KGL0xBgPLyNJA1xw==", + "dependencies": { + "estree-util-is-identifier-name": "^2.0.0", + "estree-util-value-to-estree": "^1.3.0", + "reading-time": "^1.3.0", + "unist-util-visit": "^3.1.0" + } + }, + "node_modules/remark-reading-time/node_modules/unist-util-visit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz", + "integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-reading-time/node_modules/unist-util-visit-parents": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz", + "integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.0.10.tgz", + "integrity": "sha512-t44QCeDKAPf1mtQH3fYpWz8IM/DyvHLjs8wUvvwMYxk5moOqCzrMSxK6HQVD0QVmVjXFavoFIPRVrMuJPKAvtg==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shiki": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.3.tgz", + "integrity": "sha512-U3S/a+b0KS+UkTyMjoNojvTgrBHjgp7L6ovhFVZsXmBGnVdQ4K4U9oK0z63w538S91ATngv1vXigHCSWOwnr+g==", + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sort-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", + "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-keys/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", + "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-to-object": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz", + "integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" + }, + "node_modules/supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==", + "dependencies": { + "has-flag": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/title": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/title/-/title-3.5.3.tgz", + "integrity": "sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==", + "dependencies": { + "arg": "1.0.0", + "chalk": "2.3.0", + "clipboardy": "1.2.2", + "titleize": "1.0.0" + }, + "bin": { + "title": "bin/title.js" + } + }, + "node_modules/titleize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-1.0.0.tgz", + "integrity": "sha512-TARUb7z1pGvlLxgPk++7wJ6aycXF3GJ0sNSBTAsTuJrQG5QuZlkUQP+zl+nbjAh4gMX9yDw9ZYklMd7vAfJKEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", + "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + }, + "node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-find-after": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-4.0.1.tgz", + "integrity": "sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz", + "integrity": "sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-3.1.1.tgz", + "integrity": "sha512-kfCqZK5YVY5yEa89tvpl7KnBBHu2c6CzMkqHUrlOqaRgGOMp0sMvwWOVrbAtj03KhovQB7i96Gda72v/EFE0vw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz", + "integrity": "sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-matter": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-3.0.1.tgz", + "integrity": "sha512-CAAIDwnh6ZdtrqAuxdElUqQRQDQgbbIrYtDYI8gCjXS1qQ+1XdLoK8FIZWxJwn0/I+BkSSZpar3SOgjemQz4fg==", + "dependencies": { + "@types/js-yaml": "^4.0.0", + "is-buffer": "^2.0.0", + "js-yaml": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-matter/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/vfile-matter/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==" + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==" + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/web-worker": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", + "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==" + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/website/package.json b/docs/website/package.json new file mode 100644 index 0000000..ac92766 --- /dev/null +++ b/docs/website/package.json @@ -0,0 +1,23 @@ +{ + "name": "website-docs", + "version": "1.0.0", + "description": "", + "main": "index.js", + "license": "MIT", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "^13.0.6", + "nextra": "latest", + "nextra-theme-docs": "latest", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "18.11.10", + "typescript": "^4.9.3" + } +} diff --git a/docs/website/pages/_meta.json b/docs/website/pages/_meta.json new file mode 100644 index 0000000..d8edb47 --- /dev/null +++ b/docs/website/pages/_meta.json @@ -0,0 +1,5 @@ +{ + "index": "Introduction", + "overview": "Overview", + "strategies": "Session Strategies" +} diff --git a/docs/website/pages/index.mdx b/docs/website/pages/index.mdx new file mode 100644 index 0000000..9a597b8 --- /dev/null +++ b/docs/website/pages/index.mdx @@ -0,0 +1,18 @@ +# Remix Auth + +![](https://raw.githubusercontent.com/sergiodxa/remix-auth/main/assets/header.png) + +Remix Auth is a complete open-source authentication solution for Remix.run applications. + +Heavily inspired by Passport.js, but completely rewrote it from scratch to work on top of the Web Fetch API. Remix Auth can be dropped in to any Remix-based application with minimal setup. + +As with Passport.js, it uses the strategy pattern to support the different authentication flows. Each strategy is published individually as a separate npm package. + +## Features + +- Full **Server-Side** Authentication +- Complete **TypeScript** Support +- **Strategy**-based Authentication +- Easily handle **success and failure** +- Implement **custom** strategies +- Supports persistent **sessions** diff --git a/docs/website/pages/overview/_meta.json b/docs/website/pages/overview/_meta.json new file mode 100644 index 0000000..dfaef25 --- /dev/null +++ b/docs/website/pages/overview/_meta.json @@ -0,0 +1,5 @@ +{ + "installation": "Installation Guide", + "quickstart": "Quickstart", + "advanced": "Advanced Usage" +} diff --git a/docs/website/pages/overview/advanced.mdx b/docs/website/pages/overview/advanced.mdx new file mode 100644 index 0000000..643e66b --- /dev/null +++ b/docs/website/pages/overview/advanced.mdx @@ -0,0 +1,112 @@ +# Advanced Usage + +### Custom redirect URL based on the user + +Say we have `/dashboard` and `/onboarding` routes, and after the user authenticates, you need to check some value in their data to know if they are onboarded or not. + +If we do not pass the `successRedirect` option to the `authenticator.authenticate` method, it will return the user data. + +Note that we will need to store the user data in the session this way. To ensure we use the correct session key, the authenticator has a `sessionKey` property. + +```ts +export async function action({ request }: ActionArgs) { + let user = await authenticator.authenticate("user-pass", request, { + failureRedirect: "/login", + }); + + // manually get the session + let session = await getSession(request.headers.get("cookie")); + // and store the user data + session.set(authenticator.sessionKey, user); + + // commit the session + let headers = new Headers({ "Set-Cookie": await commitSession(session) }); + + // and do your validation to know where to redirect the user + if (isOnboarded(user)) return redirect("/dashboard", { headers }); + return redirect("/onboarding", { headers }); +} +``` + +### Changing the session key + +If we want to change the session key used by Remix Auth to store the user data, we can customize it when creating the `Authenticator` instance. + +```ts +export let authenticator = new Authenticator(sessionStorage, { + sessionKey: "accessToken", +}); +``` + +With this, both `authenticate` and `isAuthenticated` will use that key to read or write the user data (in this case, the access token). + +If we need to read or write from the session manually, remember always to use the `authenticator.sessionKey` property. If we change the key in the `Authenticator` instance, we will not need to change it in the code. + +### Reading authentication errors + +When the user cannot authenticate, the error will be set in the session using the `authenticator.sessionErrorKey` property. + +We can customize the name of the key when creating the `Authenticator` instance. + +```ts +export let authenticator = new Authenticator(sessionStorage, { + sessionErrorKey: "my-error-key", +}); +``` + +Furthermore, we can read the error using that key after a failed authentication. + +```ts +// in the loader of the login route +export async function loader({ request }: LoaderArgs) { + await authenticator.isAuthenticated(request, { + successRedirect: "/dashboard", + }); + let session = await getSession(request.headers.get("cookie")); + let error = session.get(authenticator.sessionErrorKey); + return json({ error }); +} +``` + +Remember always to use the `authenticator.sessionErrorKey` property. If we change the key in the `Authenticator` instance, we will not need to change it in the code. + +### Errors Handling + +By default, any error in the authentication process will throw a Response object. If `failureRedirect` is specified, this will always be a redirect response with the error message on the `sessionErrorKey`. + +If a `failureRedirect` is not defined, Remix Auth will throw a 401 Unauthorized response with a JSON body containing the error message. This way, we can use the CatchBoundary component of the route to render any error message. + +If we want to get an error object inside the action instead of throwing a Response, we can configure the `throwOnError` option to `true`. We can do this when instantiating the `Authenticator` or calling `authenticate`. + +If we do it in the `Authenticator,` it will be the default behavior for all the `authenticate` calls. + +```ts +export let authenticator = new Authenticator(sessionStorage, { + throwOnError: true, +}); +``` + +Alternatively, we can do it on the action itself. + +```ts +import { AuthorizationError } from "remix-auth"; + +export async function action({ request }: ActionArgs) { + try { + return await authenticator.authenticate("user-pass", request, { + successRedirect: "/dashboard", + throwOnError: true, + }); + } catch (error) { + // Because redirects work by throwing a Response, you need to check if the + // caught error is a response and return it or throw it again + if (error instanceof Response) return error; + if (error instanceof AuthorizationError) { + // here the error is related to the authentication process + } + // here the error is a generic error that another reason may throw + } +} +``` + +If we define both `failureRedirect` and `throwOnError`, the redirect will happen instead of throwing an error. diff --git a/docs/website/pages/overview/installation.mdx b/docs/website/pages/overview/installation.mdx new file mode 100644 index 0000000..15f7483 --- /dev/null +++ b/docs/website/pages/overview/installation.mdx @@ -0,0 +1,85 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Installation + +## Package Setup + + + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth + ``` + + + ```bash copy + yarn add remix-auth + ``` + + + ```bash copy + pnpm install remix-auth + ``` + + + +{/* prettier-ignore-end */} + +### Create Session Storage + +```ts copy filename="app/services/session.server.ts" +import { createCookieSessionStorage } from "@remix-run/node"; + +// export the whole sessionStorage object +export let sessionStorage = createCookieSessionStorage({ + cookie: { + name: "_session", // use any name you want here + sameSite: "lax", // this helps with CSRF + path: "/", // remember to add this so the cookie will work in all routes + httpOnly: true, // for security reasons, make this cookie http only + secrets: ["s3cr3t"], // replace this with an actual secret + secure: process.env.NODE_ENV === "production", // enable this in prod only + }, +}); + +// you can also export the methods individually for your own usage +export let { getSession, commitSession, destroySession } = sessionStorage; +``` + + + Remix Auth needs a session storage object to store the user session. It can be any object that implements the SessionStorage interface from Remix. + +In this example the createCookieSessionStorage function is being used. + + + +### Auth configuration + +Now, create a file for the Remix Auth configuration. Here import the Authenticator class and your sessionStorage object. + +```ts copy filename="app/services/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { sessionStorage } from "~/services/session.server"; + +// Create an instance of the authenticator, pass a generic with what +// strategies will return and will store in the session +export let authenticator = new Authenticator(sessionStorage); +``` + + +The User type is whatever you will store in the session storage to identify the authenticated user. It can be the complete user data or a string with a token. It is completely configurable. + + + +### Choose a Strategy + +You have now created the baselayer for the Remix-Auth package and you will now have to chose one or multiple Auth Strategies + + diff --git a/docs/website/pages/overview/quickstart.mdx b/docs/website/pages/overview/quickstart.mdx new file mode 100644 index 0000000..b3c67f4 --- /dev/null +++ b/docs/website/pages/overview/quickstart.mdx @@ -0,0 +1,171 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Quickstart Guide + + +### Create Session Storage + +```tsx copy filename="app/services/session.server.ts" +import { createCookieSessionStorage } from "@remix-run/node"; + +// export the whole sessionStorage object +export let sessionStorage = createCookieSessionStorage({ + cookie: { + name: "_session", // use any name you want here + sameSite: "lax", // this helps with CSRF + path: "/", // remember to add this so the cookie will work in all routes + httpOnly: true, // for security reasons, make this cookie http only + secrets: ["s3cr3t"], // replace this with an actual secret + secure: process.env.NODE_ENV === "production", // enable this in prod only + }, +}); + +// you can also export the methods individually for your own usage +export let { getSession, commitSession, destroySession } = sessionStorage; +``` + +### Create Authenticator instance + +```tsx copy filename="app/services/auth.server.ts" +// app/services/auth.server.ts +import { Authenticator } from "remix-auth"; +import { sessionStorage } from "~/services/session.server"; + +// Create an instance of the authenticator, pass a generic with what +// strategies will return and will store in the session +export let authenticator = new Authenticator(sessionStorage); +``` + +### Install [Form Strategy](http://localhost:3000/stratgies/form) + +In this example, we will use the FormStrategy to check the documentation of the strategy you want to use to see any configuration you may need. + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-form + ``` + + + ```bash copy + yarn add remix-auth-form + ``` + + + ```bash copy + pnpm install remix-auth-form + ``` + + + +{/* prettier-ignore-end */} + +```tsx copy filename="app/services/auth.server.ts" +import { FormStrategy } from "remix-auth-form"; + +// Tell the Authenticator to use the form strategy +authenticator.use( + new FormStrategy(async ({ form }) => { + let email = form.get("email"); + let password = form.get("password"); + let user = await login(email, password); + // the type of this user must match the type you pass to the Authenticator + // the strategy will automatically inherit the type if you instantiate + // directly inside the `use` method + return user; + }), + // each strategy has a name and can be changed to use another one + // same strategy multiple times, especially useful for the OAuth2 strategy. + "user-pass" +); +``` + +Now that at least one strategy is registered, it is time to set up the routes. + +### Routes Setup + +First, create a /login page. Here we will render a form to get the email and password of the user and use Remix Auth to authenticate the user. + +```tsx copy filename="app/routes/login.tsx" +// app/routes/login.tsx +import type { ActionArgs, LoaderArgs } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; +import { Form } from "@remix-run/react"; +import { authenticator } from "~/services/auth.server"; + +// First we create our UI with the form doing a POST and the inputs with the +// names we are going to use in the strategy +export default function Screen() { + return ( +
+ + + +
+ ); +} + +// Second, we need to export an action function, here we will use the +// `authenticator.authenticate method` +export async function action({ request }: ActionArgs) { + // we call the method with the name of the strategy we want to use and the + // request object, optionally we pass an object with the URLs we want the user + // to be redirected to after a success or a failure + return await authenticator.authenticate("user-pass", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +} + +// Finally, we can export a loader function where we check if the user is +// authenticated with `authenticator.isAuthenticated` and redirect to the +// dashboard if it is or return null if it's not +export async function loader({ request }: LoaderArgs) { + // If the user is already authenticated redirect to /dashboard directly + return await authenticator.isAuthenticated(request, { + successRedirect: "/dashboard", + }); +} +``` + +With this, we have our login page. If we need to get the user data in another route of the application, we can use the authenticator.isAuthenticated method passing the request this way: + +```tsx copy +// get the user data or redirect to /login if it failed +let user = await authenticator.isAuthenticated(request, { + failureRedirect: "/login", +}); + +// if the user is authenticated, redirect to /dashboard +await authenticator.isAuthenticated(request, { + successRedirect: "/dashboard", +}); + +// get the user or null, and do different things in your loader/action based on +// the result +let user = await authenticator.isAuthenticated(request); +if (user) { + // here the user is authenticated +} else { + // here the user is not authenticated +} +``` + +Once the user is ready to leave the application, we can call the logout method inside an action. + +```tsx copy +export async function action({ request }: ActionArgs) { + await authenticator.logout(request, { redirectTo: "/login" }); +} +``` + +
diff --git a/docs/website/pages/strategies/_meta.json b/docs/website/pages/strategies/_meta.json new file mode 100644 index 0000000..1fd46b0 --- /dev/null +++ b/docs/website/pages/strategies/_meta.json @@ -0,0 +1,25 @@ +{ + "form": "Form Strategy", + "auth-socials": "Auth Socials Strategy", + "otp": "OTP/Magic Strategy", + "github": "Github Strategy", + "google": "Google Strategy", + "email": "Email Link Strategy", + "auth0": "Auth0 Strategy", + "microsoft": "Microsoft Strategy", + "steam": "Steam Strategy", + "gitlab": "Gitlab Strategy", + "bitbucket": "Bitbucket Strategy", + "spotify": "Spotify Strategy", + "twitter": "Twitter Strategy", + "linkedin": "Linkedin Strategy", + "google-credential": "Google Credential Strategy", + "okta": "Okta Strategy", + "notion": "Notion", + "twitch": "Twitch Strategy", + "keycloak": "Keycloak Strategy", + "bungie": "Bungie Strategy", + "discord": "Discord Strategy", + "twillio": "Twillio Strategy", + "web-authn": "WebAuthn (Passkey) Strategy" +} diff --git a/docs/website/pages/strategies/auth-socials.mdx b/docs/website/pages/strategies/auth-socials.mdx new file mode 100644 index 0000000..faa960f --- /dev/null +++ b/docs/website/pages/strategies/auth-socials.mdx @@ -0,0 +1,248 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Remix Auth Socials Strategy + + +A collection of Remix Auth strategies for Oauth2 Social logins. + + + +It's rare to see only one social login button, and no one likes a big package.json so here we are 👀 + +Remix auth socials collates community Oauth packages in a way that allows you to set up multiple social logins with ease. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## The Collection: + +Please visit the repo's of each package to understand the specifics on their usage, and raise issues. + +[remix-auth-discord](https://github.com/JonnyBnator/remix-auth-discord) - By [Jonny](https://github.com/JonnyBnator) + +[remix-auth-facebook](https://github.com/manosim/remix-auth-facebook) - By [Manos](https://github.com/manosim) + +[remix-auth-github](https://github.com/sergiodxa/remix-auth-github) - By [Sergio](https://github.com/sergiodxa) + +[remix-auth-google](https://github.com/pbteja1998/remix-auth-google) - By [Bhanu](https://github.com/pbteja1998) + +[remix-auth-microsoft](https://github.com/juhanakristian/remix-auth-microsoft) - By [Juhana](https://github.com/juhanakristian) + +[remix-auth-twitter](https://github.com/na2hiro/remix-auth-twitter) - By [na2hiro](https://github.com/na2hiro) + + + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-socials + ``` + + + ```bash copy + yarn add remix-auth-socials + ``` + + + ```bash copy + pnpm install remix-auth-socials + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/utils/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { + GoogleStrategy, + FacebookStrategy, + SocialsProvider, +} from "remix-auth-socials"; +import { sessionStorage } from "~/services/session.server"; + +// Create an instance of the authenticator +export let authenticator = new Authenticator(sessionStorage, { + sessionKey: "_session", +}); +// You may specify a type which the strategies will return (this will be stored in the session) +// export let authenticator = new Authenticator(sessionStorage, { sessionKey: '_session' }); + +const getCallback = (provider: SocialsProvider) => { + return `http://localhost:3333/auth/${provider}/callback`; +}; + +authenticator.use( + new GoogleStrategy( + { + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: getCallback(SocialsProvider.GOOGLE), + }, + async ({ profile }) => { + // here you would find or create a user in your database + return profile; + } + ) +); + +authenticator.use( + new FacebookStrategy( + { + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: getCallback(SocialsProvider.FACEBOOK), + }, + async ({ profile }) => {} + ) +); +``` + +### Setup your routes + +```tsx copy filename="app/routes/auth/$provider.tsx" +import { ActionArgs, redirect } from "@remix-run/node"; +import { authenticator } from "~/server/auth.server"; + +export let loader = () => redirect("/login"); + +export let action = ({ request, params }: ActionArgs) => { + return authenticator.authenticate(params.provider, request); +}; +``` + +```tsx copy filename="app/routes/auth/$provider.callback.tsx" +import { LoaderArgs } from "@remix-run/node"; +import { authenticator } from "~/server/auth.server"; + +export let loader = ({ request, params }: LoaderArgs) => { + return authenticator.authenticate(params.provider, request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +```tsx copy filename="app/routes/login.tsx" +import { Form } from "@remix-run/react"; +import { SocialsProvider } from "remix-auth-socials"; + +interface SocialButtonProps { + provider: SocialsProvider; + label: string; +} + +const SocialButton: React.FC = ({ provider, label }) => ( +
+ +
+); + +export default function Login() { + return ( + <> + + + + + + + ); +} +``` + +```tsx copy filename="app/routes/logout.tsx" +import { ActionArgs } from "@remix-run/node"; +import { authenticator } from "~/server/auth.server"; + +export let action = async ({ request, params }: ActionArgs) => { + await authenticator.logout(request, { redirectTo: "/" }); +}; +``` + +### Add a protected route and an automatic success redirect + +Here's an example of a protected route + +```tsx copy filename="app/routes/dashboard.tsx" +import { useLoaderData, Form } from "@remix-run/react"; +import { LoaderArgs } from "@remix-run/node"; +import { authenticator } from "~/server/auth.server"; + +export let loader = async ({ request, params }: LoaderArgs) => { + const user = await authenticator.isAuthenticated(request, { + failureRedirect: "/", + }); + + return { user }; +}; + +export default function Dashboard() { + const { user } = useLoaderData(); + + return ( +
+

Welcome {user.displayName}!

+

This is a protected page

+
+ +
+
+ ); +} +``` + +You might also want your index route to redirect to the dashboard for logged in users. + +```tsx copy filename="app/routes/index.tsx" +import { useLoaderData } from "@remix-run/react"; +import { LoaderArgs } from "@remix-run/node"; +import { authenticator } from "~/server/auth.server"; + +export let loader = async ({ request, params }: LoaderArgs) => { + const user = await authenticator.isAuthenticated(request, { + successRedirect: "/dashboard", + }); + return user; +}; + +export default function Index() { + return ( +
+

Welcome!

+

+ Please log in +

+
+ ); +} +``` + +
diff --git a/docs/website/pages/strategies/auth0.mdx b/docs/website/pages/strategies/auth0.mdx new file mode 100644 index 0000000..a7d86f9 --- /dev/null +++ b/docs/website/pages/strategies/auth0.mdx @@ -0,0 +1,137 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Auth0 Strategy + +The Auth0 strategy is used to authenticate users against an Auth0 account. It extends the OAuth2Strategy. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Create an Auth0 tenant + +Follow the steps on [the Auth0 documentation](https://auth0.com/docs/get-started/create-tenants) to create a tenant and get a client ID, client secret and domain. + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-auth0 + ``` + + + ```bash copy + yarn add remix-auth-auth0 + ``` + + + ```bash copy + pnpm install remix-auth-auth0 + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/utils/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { Auth0Strategy } from "remix-auth-auth0"; + +// Create an instance of the authenticator, pass a generic with what your +// strategies will return and will be stored in the session +export const authenticator = new Authenticator(sessionStorage); + +let auth0Strategy = new Auth0Strategy( + { + callbackURL: "https://example.com/auth/auth0/callback", + clientID: "YOUR_AUTH0_CLIENT_ID", + clientSecret: "YOUR_AUTH0_CLIENT_SECRET", + domain: "YOUR_TENANT.us.auth0.com", + }, + async ({ accessToken, refreshToken, extraParams, profile }) => { + // Get the user data from your DB or API using the tokens and profile + return User.findOrCreate({ email: profile.emails[0].value }); + } +); + +authenticator.use(auth0Strategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```tsx copy filename="app/routes/auth/auth0/callback.tsx" +import type { LoaderArgs } from "@remix-run/node"; + +import { authenticator } from "~/utils/auth.server"; + +export let loader = ({ request }: LoaderArgs) => { + return authenticator.authenticate("auth0", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +```tsx copy filename="app/routes/auth/logout.ts" +import type { ActionArgs } from "@remix-run/node"; + +import { redirect } from "@remix-run/node"; + +import { destroySession, getSession } from "~/utils/auth.server"; + +export const action = async ({ request }: ActionArgs) => { + const session = await getSession(request.headers.get("Cookie")); + const logoutURL = new URL(process.env.AUTH0_LOGOUT_URL); // i.e https://YOUR_TENANT.us.auth0.com/v2/logout + + logoutURL.searchParams.set("client_id", process.env.AUTH0_CLIENT_ID); + logoutURL.searchParams.set("returnTo", process.env.AUTH0_RETURN_TO_URL); + + return redirect(logoutURL.toString(), { + headers: { + "Set-Cookie": await destroySession(session), + }, + }); +}; +``` + +
+ +## Advanced Usage + +### Link directly to signup + +```tsx copy filename="app/routes/register.tsx" +export default function Register() { + return ( +
+ +
+ ); +} + +// https://auth0.com/docs/authenticate/login/auth0-universal-login/new-experience#signup +``` diff --git a/docs/website/pages/strategies/bitbucket.mdx b/docs/website/pages/strategies/bitbucket.mdx new file mode 100644 index 0000000..75101c1 --- /dev/null +++ b/docs/website/pages/strategies/bitbucket.mdx @@ -0,0 +1,99 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Bitbucket Strategy + +The Bitbucket Cloud strategy for [remix-auth](https://github.com/sergiodxa/remix-auth) is used to authenticate users against a Bitbucket Cloud account. It extends the OAuth2Strategy. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-bitbucket + ``` + + + ```bash copy + yarn add remix-auth-bitbucket + ``` + + + ```bash copy + pnpm install remix-auth-bitbucket + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { BitbucketStrategy } from "remix-auth-bitbucket"; + +let bitbucketStrategy = new BitbucketStrategy( + { + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: "https://example.com/auth/bitbucket/callback", + }, + async ({ accessToken, extraParams, profile }) => { + // Get the user data from your DB or API using the tokens and profile + return User.findOrCreate({ email: profile.emails[0].value }); + } +); + +authenticator.use(bitbucketStrategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```tsx copy filename="app/routes/auth/bitbucket.tsx" +import { ActionFunction, LoaderFunction, redirect } from "remix"; +import { authenticator } from "~/auth.server"; + +export let loader: LoaderFunction = () => redirect("/login"); + +export let action: ActionFunction = ({ request }) => { + return authenticator.authenticate("bitbucket", request); +}; +``` + +```tsx copy filename="app/routes/auth/bitbucket/callback.tsx" +import { LoaderFunction } from "remix"; +import { authenticator } from "~/auth.server"; + +export let loader: LoaderFunction = ({ request }) => { + return authenticator.authenticate("bitbucket", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +
diff --git a/docs/website/pages/strategies/bungie.mdx b/docs/website/pages/strategies/bungie.mdx new file mode 100644 index 0000000..3f10eaf --- /dev/null +++ b/docs/website/pages/strategies/bungie.mdx @@ -0,0 +1,132 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Bungie Strategy + +This is a [Bungie](https://bungie.net/) strategy for [remix-auth](https://github.com/sergiodxa/remix-auth) library. + +This is based off of the Google Strategy from [remix-auth-socials](https://github.com/TheRealFlyingCoder/remix-auth-socials) and the Steam Strategy from [remix-auth-steam](https://github.com/Andreychik32/remix-auth-steam). + + +Bungie requires the callback to use `https://` meaning you can't use the default `http://localhost:3000` even for development. The solution that seems to have the least friction and the one I used was the free plan from [ngrok](https://ngrok.com/). You can use any solution you like, but we aware that you can't use just `http://`. + +It also seems you can't use `localhost` but may need to use `127.0.0.1` or some domain setup in your hosts file. I didn't test this can not confirm either way. Using ngrok produces a domain name anyway, so if that is a limitation ngrok also solved that. + +For the sake of this README I will assume that no matter the solution you use, you have a URL saved to your .env file. You will need to update your cookie to use the domain assigned to your ngrok tunnel, if you use that solution. If you have a URL in .env, you can set it to that. + + + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ❓ | + +## Setup Guide + + + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-bungie + ``` + + + ```bash copy + yarn add remix-auth-bungie + ``` + + + ```bash copy + pnpm install remix-auth-bungie + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { sessionStorage } from "~/services/session.server"; +import { BungieStrategy } from "remix-auth-bungie"; +import type { BungieProfile } from "remix-auth-bungie"; + +// you can import User elsewhere to type the profile +export type User = BungieProfile; + +export let authenticator = new Authenticator(sessionStorage); + +if ( + !process.env.BUNGIE_ID || + !process.env.BUNGIE_SECRET || + !process.env.BUNGIE_APIKEY +) { + throw new Error("Bungie ID, Secret and API Key are required"); +} + +authenticator.use( + new BungieStrategy( + { + clientID: process.env.BUNGIE_ID, + clientSecret: process.env.BUNGIE_SECRET, + callbackURL: `https://${process.env.CALLBACK_URL}/auth/callback/bungie`, + apiKey: process.env.BUNGIE_APIKEY, + }, + async ({ profile }) => { + return profile; + } + ) +); +``` + +### Setup your routes + +```tsx copy filename="app/routes/auth/bungie.tsx" +import type { ActionFunction, LoaderFunction } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; +import { authenticator } from "~/services/auth.server"; + +export let loader: LoaderFunction = () => redirect("/login"); + +export let action: ActionFunction = ({ request, params }) => { + return authenticator.authenticate("bungie", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +```tsx copy filename="app/routes/auth/callback/bungie.tsx" +import type { LoaderFunction } from "@remix-run/node"; +import { authenticator } from "~/services/auth.server"; + +export let loader: LoaderFunction = ({ request, params }) => { + return authenticator.authenticate("bungie", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +```tsx copy filename="app/routes/auth/linkedin/callback.tsx" +import { ActionFunction, LoaderFunction } from "remix"; +import { authenticator } from "~/linkedin.server"; + +export let loader: LoaderFunction = ({ request }) => { + return authenticator.authenticate("linkedin", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + + diff --git a/docs/website/pages/strategies/discord.mdx b/docs/website/pages/strategies/discord.mdx new file mode 100644 index 0000000..17ca253 --- /dev/null +++ b/docs/website/pages/strategies/discord.mdx @@ -0,0 +1,167 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Discord Strategy + +The Discord strategy is used to authenticate users against a Discord account. It extends the [OAuth2Strategy](https://github.com/sergiodxa/remix-auth-oauth2). + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Create an OAuth application + +First go to [the Discord Developer Portal](https://discord.com/developers/applications) to create a new application and get a client ID and secret. The client ID and secret are located in the OAuth2 Tab of your Application. Once you are there you can already add your first redirect url, f.e. `http://localhost:3000/auth/discord/callback`. + +You can find the detailed Discord OAuth Documentation [here](https://discord.com/developers/docs/topics/oauth2#oauth2). + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-linkedin + ``` + + + ```bash copy + yarn add remix-auth-linkedin + ``` + + + ```bash copy + pnpm install remix-auth-linkedin + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { Authenticator } from "remix-auth"; +import type { DiscordProfile, PartialDiscordGuild } from "remix-auth-discord"; +import { DiscordStrategy } from "remix-auth-discord"; +import { sessionStorage } from "~/session.server"; + +export interface DiscordUser { + id: DiscordProfile["id"]; + displayName: DiscordProfile["displayName"]; + avatar: DiscordProfile["__json"]["avatar"]; + discriminator: DiscordProfile["__json"]["discriminator"]; + email: DiscordProfile["__json"]["email"]; + guilds?: Array; + accessToken: string; + refreshToken: string; +} + +export const auth = new Authenticator(sessionStorage); + +const discordStrategy = new DiscordStrategy( + { + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: "https://example.com/auth/discord/callback", + // Provide all the scopes you want as an array + scope: ["identify", "email", "guilds"], + }, + async ({ + accessToken, + refreshToken, + extraParams, + profile, + }): Promise => { + /** + * Get the user data from your DB or API using the tokens and profile + * For example query all the user guilds + * IMPORTANT: This can quickly fill the session storage to be too big. + * So make sure you only return the values from the guilds (and the guilds) you actually need + * (eg. omit the features) + * and if that's still to big, you need to store the guilds some other way. (Your own DB) + * + * Either way, this is how you could retrieve the user guilds. + */ + const userGuilds: Array = await ( + await fetch("https://discord.com/api/v10/users/@me/guilds", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + )?.json(); + /** + * In this example we're only interested in guilds where the user is either the owner or has the `MANAGE_GUILD` permission (This check includes the `ADMINISTRATOR` permission) + */ + const guilds: Array = userGuilds.filter( + (g) => g.owner || (BigInt(g.permissions) & BigInt(0x20)) == BigInt(0x20) + ); + + /** + * Construct the user profile to your liking by adding data you fetched etc. + * and only returning the data that you actually need for your application. + */ + return { + id: profile.id, + displayName: profile.__json.username, + avatar: profile.__json.avatar, + discriminator: profile.__json.discriminator, + email: profile.__json.email, + accessToken, + guilds, + refreshToken, + }; + } +); + +auth.use(discordStrategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +import { Form } from "@remix-run/react"; + +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```tsx copy filename="app/routes/auth/discord.tsx" +import type { ActionFunction, LoaderFunction } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; +import { auth } from "~/auth.server"; + +export let loader: LoaderFunction = () => redirect("/login"); + +export let action: ActionFunction = ({ request }) => { + return auth.authenticate("discord", request); +}; +``` + +```tsx copy filename="app/routes/auth/discord.callback.tsx" +import type { LoaderFunction } from "@remix-run/node"; +import { auth } from "~/auth.server"; + +export let loader: LoaderFunction = ({ request }) => { + return auth.authenticate("discord", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +
diff --git a/docs/website/pages/strategies/email.mdx b/docs/website/pages/strategies/email.mdx new file mode 100644 index 0000000..8ca7f98 --- /dev/null +++ b/docs/website/pages/strategies/email.mdx @@ -0,0 +1,350 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Email Link Strategy + + +This strategy is heavily based on kcd strategy present in the [v2 of Remix Auth](https://github.com/sergiodxa/remix-auth/blob/v2.6.0/docs/strategies/kcd.md). The major difference being we are using crypto-js instead of crypto so that it can be deployed on CF. + + + +The Email Link Strategy implements the authentication strategy used on [kentcdodds.com](https://kentcdodds.com). + +This strategy uses passwordless flow with magic links. A magic link is a special URL generated when the user tries to login, this URL is sent to the user via email, after the click on it the user is automatically logged in. + +You can read more about how this work in the [kentcdodds.com/how-i-built-a-modern-website-in-2021](https://kentcdodds.com/blog/how-i-built-a-modern-website-in-2021#authentication-with-magic-links). + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + +Because of how this strategy works you need a little bit more setup than other strategies, but nothing specially crazy. + + +### Email Service +import { Tab, Tabs } from "nextra-theme-docs"; + +You will need to have some email service configured in your application. What you actually use to send emails is not important, as far as you can create a function with this type: + +```ts copy +type SendEmailOptions = { + emailAddress: string; + magicLink: string; + user?: User | null; + domainUrl: string; + form: FormData; +}; + +type SendEmailFunction = { + (options: SendEmailOptions): Promise; +}; +``` + +So if you have something like `app/services/email-provider.server.ts` file exposing a generic function like `sendEmail` function receiving an email address, subject and body, you could use it like this: + +```ts copy filename="app/services/email.server.tsx" +import { renderToString } from "react-dom/server"; +import type { SendEmailFunction } from "remix-auth-email-link"; +import type { User } from "~/models/user.model"; +import * as emailProvider from "~/services/email-provider.server"; + +export let sendEmail: SendEmailFunction = async (options) => { + let subject = "Here's your Magic sign-in link"; + let body = renderToString( +

+ Hi {options.user?.name || "there"},
+
+ Click here to login on example.app +

+ ); + + await emailProvider.sendEmail(options.emailAddress, subject, body); +}; +``` + + +Again, what you use as email provider is not important, you could use a third party service like [Mailgun](https://mailgun.com) or [Sendgrid](https://sendgrid.com), if you are using AWS you could use SES. + + + +### Create the strategy instance + +Now that you have your sendEmail email function you can create an instance of the Authenticator and the EmailLinkStrategy. + +```ts copy filename="app/services/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { EmailLinkStrategy } from "remix-auth-email-link"; +import { sessionStorage } from "~/services/session.server"; +import { sendEmail } from "~/services/email.server"; +import { User, getUserByEmail } from "~/models/user.server"; + +// This secret is used to encrypt the token sent in the magic link and the +// session used to validate someone else is not trying to sign-in as another +// user. +let secret = process.env.MAGIC_LINK_SECRET; +if (!secret) throw new Error("Missing MAGIC_LINK_SECRET env variable."); + +export let auth = new Authenticator(sessionStorage); + +// Here we need the sendEmail, the secret and the URL where the user is sent +// after clicking on the magic link +auth.use( + new EmailLinkStrategy( + { sendEmail, secret, callbackURL: "/magic" }, + // In the verify callback, + // you will receive the email address, form data and whether or not this is being called after clicking on magic link + // and you should return the user instance + async ({ + email, + form, + magicLinkVerify, + }: { + email: string; + form: FormData; + magicLinkVerify: boolean; + }) => { + let user = await getUserByEmail(email); + return user; + } + ) +); +``` + +### Setup your routes + +Now you can proceed to create your routes and do the setup. + +```ts copy filename="app/routes/login.tsx" +import { ActionArgs, LoaderArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { Form, useLoaderData } from "@remix-run/react"; +import { auth } from "~/services/auth.server"; +import { sessionStorage } from "~/services/session.server"; + +export let loader = async ({ request }: LoaderArgs) => { + await auth.isAuthenticated(request, { successRedirect: "/me" }); + let session = await sessionStorage.getSession(request.headers.get("Cookie")); + // This session key `auth:magiclink` is the default one used by the EmailLinkStrategy + // you can customize it passing a `sessionMagicLinkKey` when creating an + // instance. + return json({ + magicLinkSent: session.has("auth:magiclink"), + magicLinkEmail: session.get("auth:email"), + }); +}; + +export let action = async ({ request }: ActionArgs) => { + // The success redirect is required in this action, this is where the user is + // going to be redirected after the magic link is sent, note that here the + // user is not yet authenticated, so you can't send it to a private page. + await auth.authenticate("email-link", request, { + successRedirect: "/login", + // If this is not set, any error will be throw and the ErrorBoundary will be + // rendered. + failureRedirect: "/login", + }); +}; + +// app/routes/login.tsx +export default function Login() { + let { magicLinkSent, magicLinkEmail } = useLoaderData(); + + return ( +
+ {magicLinkSent ? ( +

+ Successfully sent magic link{" "} + {magicLinkEmail ? `to ${magicLinkEmail}` : ""} +

+ ) : ( + <> +

Log in to your account.

+
+ + +
+ + + )} +
+ ); +} +``` + +```ts copy filename="app/routes/magic.tsx" +import { LoaderArgs } from "@remix-run/node"; +import { auth } from "~/services/auth.server"; + +export let loader = async ({ request }: LoaderArgs) => { + await auth.authenticate("email-link", request, { + // If the user was authenticated, we redirect them to their profile page + // This redirect is optional, if not defined the user will be returned by + // the `authenticate` function and you can render something on this page + // manually redirect the user. + successRedirect: "/me", + // If something failed we take them back to the login page + // This redirect is optional, if not defined any error will be throw and + // the ErrorBoundary will be rendered. + failureRedirect: "/login", + }); +}; +``` + +```ts copy filename="app/routes/me.tsx" +import { LoaderArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { auth } from "~/services/auth.server"; + +export let loader = async ({ request }: LoaderArgs) => { + // If the user is here, it's already authenticated, if not redirect them to + // the login page. + let user = await auth.isAuthenticated(request, { failureRedirect: "/login" }); + return json({ user }); +}; + +export default function Me() { + let { user } = useLoaderData(); + return ( +
+

Welcome {user.name}

+

You are logged in as {user.email}

+
+ ); +} +``` + +
+ +## Email validation + +The EmailLinkStrategy also supports email validation, this is useful if you want to prevent someone from signing-in with a disposable email address or you have some denylist of emails for some reason. + +By default, the EmailStrategy will validate every email against the regular expression `/.+@.+/`, if it doesn't pass it will throw an error. + +If you want to customize it you can create a function with this type and pass it to the EmailLinkStrategy. + +```ts copy f +type VerifyEmailFunction = { + (email: string): Promise; +}; +``` + +### Example + +```ts copy filename="app/services/verifier.server.ts" +import { VerifyEmailFunction } from "remix-auth-email-link"; +import { isEmailBurner } from "burner-email-providers"; +import isEmail from "validator/lib/isEmail"; + +export let verifyEmailAddress: VerifyEmailFunction = async (email) => { + if (!isEmail(email)) throw new Error("Invalid email address."); + if (isEmailBurner(email)) throw new Error("Email not allowed."); +}; +``` + +```ts copy filename="app/services/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { Authenticator, EmailLinkStrategy } from "remix-auth-email-link"; +import { sessionStorage } from "~/services/session.server"; +import { sendEmail } from "~/services/email.server"; +import { User, getUserByEmail } from "~/models/user.model"; +import { verifyEmailAddress } from "~/services/verifier.server"; + +// This secret is used to encrypt the token sent in the magic link and the +// session used to validate someone else is not trying to sign-in as another +// user. +let secret = process.env.MAGIC_LINK_SECRET; +if (!secret) throw new Error("Missing MAGIC_LINK_SECRET env variable."); + +let auth = new Authenticator(sessionStorage); + +// Here we need the sendEmail, the secret and the URL where the user is sent +// after clicking on the magic link +auth.use( + new EmailLinkStrategy( + { verifyEmailAddress, sendEmail, secret, callbackURL: "/magic" }, + // In the verify callback you will only receive the email address and you + // should return the user instance + async ({ email }: { email: string }) => { + let user = await getUserByEmail(email); + return user; + } + ) +); +``` + +## Optional Configuration + +The EmailLinkStrategy supports a few more optional configuration options you can set. Here's the whole type with each option commented. + +```ts +type EmailLinkStrategyOptions = { + /** + * The endpoint the user will go after clicking on the email link. + * A whole URL is not required, the pathname is enough, the strategy will + * detect the host of the request and use it to build the URL. + * @default "/magic" + */ + callbackURL?: string; + /** + * A function to send the email. This function should receive the email + * address of the user and the URL to redirect to and should return a Promise. + * The value of the Promise will be ignored. + */ + sendEmail: SendEmailFunction; + /** + * A function to validate the email address. This function should receive the + * email address as a string and return a Promise. The value of the Promise + * will be ignored, in case of error throw an error. + * + * By default it only test the email against the RegExp `/.+@.+/`. + */ + verifyEmailAddress?: VerifyEmailFunction; + /** + * A secret string used to encrypt and decrypt the token and magic link. + */ + secret: string; + /** + * The name of the form input used to get the email. + * @default "email" + */ + emailField?: string; + /** + * The param name the strategy will use to read the token from the email link. + * @default "token" + */ + magicLinkSearchParam?: string; + /** + * How long the magic link will be valid. Default to 30 minutes. + * @default 1_800_000 + */ + linkExpirationTime?: number; + /** + * The key on the session to store any error message. + * @default "auth:error" + */ + sessionErrorKey?: string; + /** + * The key on the session to store the magic link. + * @default "auth:magiclink" + */ + sessionMagicLinkKey?: string; + /** + * Add an extra layer of protection and validate the magic link is valid. + * @default false + */ + validateSessionMagicLink?: boolean; + + /** + * The key on the session to store the email. + * It's unset the same time the sessionMagicLinkKey is. + * @default "auth:email" + */ + sessionEmailKey?: string; +}; +``` diff --git a/docs/website/pages/strategies/form.mdx b/docs/website/pages/strategies/form.mdx new file mode 100644 index 0000000..06b4186 --- /dev/null +++ b/docs/website/pages/strategies/form.mdx @@ -0,0 +1,111 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Form Strategy + +A Remix Auth strategy to work with any form. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-form + ``` + + + ```bash copy + yarn add remix-auth-form + ``` + + + ```bash copy + pnpm install remix-auth-form + ``` + + + +{/* prettier-ignore-end */} + +### Initialize Form Strategy + +```ts copy filename="app/services/auth.server.ts" +import { FormStrategy } from "remix-auth-form"; + +// The rest of the code above here... + +authenticator.use( + new FormStrategy(async ({ form, context }) => { + // Here you can use `form` to access and input values from the form. + // and also use `context` to access more things from the server + let username = form.get("username"); // or email... etc + let password = form.get("password"); + + // You can validate the inputs however you want + invariant(typeof username === "string", "username must be a string"); + invariant(username.length > 0, "username must not be empty"); + + invariant(typeof password === "string", "password must be a string"); + invariant(password.length > 0, "password must not be empty"); + + // And if you have a password you should hash it + let hashedPassword = await hash(password); + + // And finally, you can find, or create, the user + let user = await findOrCreateUser(username, hashedPassword); + + // And return the user as the Authenticator expects it + return user; + }) +); +``` + +### Authenticate User + +In order to authenticate a user, you can use the following inside of an action function: + +```ts copy filename="action-route" +export async function action({ context, request }: ActionArgs) { + return await authenticator.authenticate("form", request, { + successRedirect: "/", + failureRedirect: "/login", + context, // optional + }); +} +``` + + + +## Passing a pre-read FormData object + +Because you may want to do validations or read values from the FormData before calling authenticate, the FormStrategy allows you to pass a FormData object as part of the optional context. + +```ts copy filename="action-route" +export async function action({ context, request }: ActionArgs) { + let formData = await request.formData(); + return await authenticator.authenticate("form", request, { + // use formData here + successRedirect: formData.get("redirectTo"), + failureRedirect: "/login", + context: { formData }, // pass pre-read formData here + }); +} +``` + + +This way, you don't need to clone the request yourself. + + diff --git a/docs/website/pages/strategies/github.mdx b/docs/website/pages/strategies/github.mdx new file mode 100644 index 0000000..4d5c2fa --- /dev/null +++ b/docs/website/pages/strategies/github.mdx @@ -0,0 +1,105 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Github Strategy + +The GitHub strategy is used to authenticate users against a GitHub account. It extends the OAuth2Strategy. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + +### Create an OAuth application + +Follow the steps on [the GitHub documentation](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app) to create a new application and get a client ID and secret. + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-github + ``` + + + ```bash copy + yarn add remix-auth-github + ``` + + + ```bash copy + pnpm install remix-auth-github + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```ts copy filename="app/services/auth.server.ts" +import { GitHubStrategy } from "remix-auth-github"; + +let gitHubStrategy = new GitHubStrategy( + { + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: "https://example.com/auth/github/callback", + }, + async ({ accessToken, extraParams, profile }) => { + // Get the user data from your DB or API using the tokens and profile + return User.findOrCreate({ email: profile.emails[0].value }); + } +); + +authenticator.use(gitHubStrategy); +``` + +### Setup your routes + +```ts copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```ts copy filename="app/routes/auth/github.tsx" +import type { ActionArgs } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; +import { authenticator } from "~/auth.server"; + +export async function loader() { + return redirect("/login"); +} + +export async function action({ request }: ActionArgs) { + return authenticator.authenticate("github", request); +} +``` + +```ts copy filename="app/routes/auth/github/callback.tsx" +import type { LoaderArgs } from "@remix-run/node"; +import { authenticator } from "~/auth.server"; + +export async function loader({ request }: LoaderArgs) { + return authenticator.authenticate("github", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +} +``` + +
diff --git a/docs/website/pages/strategies/gitlab.mdx b/docs/website/pages/strategies/gitlab.mdx new file mode 100644 index 0000000..14a1ac7 --- /dev/null +++ b/docs/website/pages/strategies/gitlab.mdx @@ -0,0 +1,99 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Gitlab Strategy + +The Gitlab strategy for [remix-auth](https://github.com/sergiodxa/remix-auth) is used to authenticate users against a Gitlab account. It extends the OAuth2Strategy. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-gitlab + ``` + + + ```bash copy + yarn add remix-auth-gitlab + ``` + + + ```bash copy + pnpm install remix-auth-gitlab + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { GitlabStrategy } from "remix-auth-gitlab"; + +let gitlabStrategy = new GitlabStrategy( + { + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: "https://example.com/auth/gitlab/callback", + }, + async ({ accessToken, extraParams, profile }) => { + // Get the user data from your DB or API using the tokens and profile + return User.findOrCreate({ email: profile.emails[0].value }); + } +); + +authenticator.use(gitlabStrategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```tsx copy filename="app/routes/auth/gitlab.tsx" +import { ActionFunction, LoaderFunction, redirect } from "remix"; +import { authenticator } from "~/auth.server"; + +export let loader: LoaderFunction = () => redirect("/login"); + +export let action: ActionFunction = ({ request }) => { + return authenticator.authenticate("gitlab", request); +}; +``` + +```tsx copy filename="app/routes/auth/gitlab/callback.tsx" +import { LoaderFunction } from "remix"; +import { authenticator } from "~/auth.server"; + +export let loader: LoaderFunction = ({ request }) => { + return authenticator.authenticate("gitlab", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +
diff --git a/docs/website/pages/strategies/google-credential.mdx b/docs/website/pages/strategies/google-credential.mdx new file mode 100644 index 0000000..ee3969c --- /dev/null +++ b/docs/website/pages/strategies/google-credential.mdx @@ -0,0 +1,73 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Google Credential Strategy + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ❓ | + +## Setup Guide + + + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-google-credential google-auth-library + ``` + + + ```bash copy + yarn add remix-auth-google-credential google-auth-library + ``` + + + ```bash copy + pnpm install remix-auth-google-credential google-auth-library + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { GoogleCredentialStrategy } from "remix-auth-google-credential"; + +// Create an instance of the authenticator, pass a generic type which the +// strategies will return (this will be stored in the session) +export let authenticator = new Authenticator(sessionStorage, { + sessionErrorKey, +}); + +authenticator.use( + new GoogleStrategy( + { + clientId: "YOUR_CLIENT_ID", + credentialId: "credential", // name of form field that stores credential. Default: credential + }, + async (profile) => { + return findOrCreateUser(profile); + } + ) +); +``` + +### How to use + + +This strategy accepts Google credential responses via FormData. This strategy supports Google one-tap html and javascript api. When using html api, set login_uri attribute to the strategy endpoint. When using javascript api, send credentials to straregy endpoint via fetcher. + + + + diff --git a/docs/website/pages/strategies/google.mdx b/docs/website/pages/strategies/google.mdx new file mode 100644 index 0000000..8dc3fb7 --- /dev/null +++ b/docs/website/pages/strategies/google.mdx @@ -0,0 +1,102 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Google Strategy + +The Google strategy is used to authenticate users against a Google account. It extends the OAuth2Strategy. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + +### Create an OAuth application + +Follow the steps on [the Google documentation](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred) to create a new application and get a client ID and secret. + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-google + ``` + + + ```bash copy + yarn add remix-auth-google + ``` + + + ```bash copy + pnpm install remix-auth-google + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```ts copy filename="app/services/auth.server.ts" +import { GoogleStrategy } from "remix-auth-google"; + +let googleStrategy = new GoogleStrategy( + { + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: "https://example.com/auth/google/callback", + }, + async ({ accessToken, refreshToken, extraParams, profile }) => { + // Get the user data from your DB or API using the tokens and profile + return User.findOrCreate({ email: profile.emails[0].value }); + } +); + +authenticator.use(googleStrategy); +``` + +### Setup your routes + +```ts copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```ts copy filename="app/routes/auth/google.tsx" +import { ActionArgs } from "@remix-run/node"; +import { authenticator } from "~/services/auth.server"; + +export let loader = () => redirect("/login"); + +export let action = ({ request }: ActionArgs) => { + return authenticator.authenticate("google", request); +}; +``` + +```ts copy filename="app/routes/auth/google/callback.tsx" +import { LoaderArgs } from "@remix-run/node"; +import { authenticator } from "~/services/auth.server"; + +export let loader = ({ request }: LoaderArgs) => { + return authenticator.authenticate("google", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +
diff --git a/docs/website/pages/strategies/keycloak.mdx b/docs/website/pages/strategies/keycloak.mdx new file mode 100644 index 0000000..c254801 --- /dev/null +++ b/docs/website/pages/strategies/keycloak.mdx @@ -0,0 +1,109 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Keycloak Strategy + +The Linkedin strategy is used to authenticate users against a Linkedin account. It extends the [OAuth2Strategy](https://github.com/sergiodxa/remix-auth-oauth2). + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-keycloak + ``` + + + ```bash copy + yarn add remix-auth-keycloak + ``` + + + ```bash copy + pnpm install remix-auth-keycloak + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/utils/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { Keycloak } from "remix-auth-keycloak"; + +// Create an instance of the authenticator, pass a generic with what your +// strategies will return and will be stored in the session +export const authenticator = new Authenticator(sessionStorage); + +let keycloakStrategy = new KeycloakStrategy( + { + useSSL: true, + domain: "example.app", + realm: "example", + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: "your.app/callback", + }, + async ({ accessToken, refreshToken, extraParams, profile }) => { + // Get the user data from your DB or API using the tokens and profile + return User.findOrCreate({ email: profile.emails[0].value }); + } +); + +authenticator.use(keycloakStrategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```tsx copy filename="app/routes/auth/keycloak.tsx" +import type { ActionFunction, LoaderFunction } from "remix"; + +import { authenticator } from "~/utils/auth.server"; + +export let loader: LoaderFunction = () => redirect("/login"); + +export let action: ActionFunction = ({ request }) => { + return authenticator.authenticate("keycloak", request); +}; +``` + +```tsx copy filename="app/routes/auth/keycloak/callback.tsx" +import type { ActionFunction, LoaderFunction } from "remix"; + +import { authenticator } from "~/utils/auth.server"; + +export let loader: LoaderFunction = ({ request }) => { + return authenticator.authenticate("keycloak", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +
diff --git a/docs/website/pages/strategies/linkedin.mdx b/docs/website/pages/strategies/linkedin.mdx new file mode 100644 index 0000000..6b0e580 --- /dev/null +++ b/docs/website/pages/strategies/linkedin.mdx @@ -0,0 +1,137 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Linkedin Strategy + +The Linkedin strategy is used to authenticate users against a Linkedin account. It extends the [OAuth2Strategy](https://github.com/sergiodxa/remix-auth-oauth2). + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Create an OAuth application + +First you need to create a new application in the [Linkedin's developers page](https://developer.linkedin.com/). Then I encourage you to read [this documentation page](https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?tabs=HTTPS#prerequisites), it explains how to configure your app and gives you useful information on the auth flow. The app is mandatory in order to obtain a `clientID` and `client secret` to use with the Linkedin's API. + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-linkedin + ``` + + + ```bash copy + yarn add remix-auth-linkedin + ``` + + + ```bash copy + pnpm install remix-auth-linkedin + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { createCookieSessionStorage } from 'remix'; +import { Authenticator } from 'remix-auth'; +import { LinkedinStrategy } from "remix-auth-linkedin"; + +// Personalize this options for your usage. +const cookieOptions = { + path: '/', + httpOnly: true, + sameSite: 'lax' as const, + maxAge: 24 * 60 * 60 * 1000 * 30, + secrets: ['THISSHOULDBESECRET_AND_NOT_SHARED'], + secure: process.env.NODE_ENV !== 'development', +}; + +const sessionStorage = createCookieSessionStorage({ + cookie: cookieOptions, +}); + +export const authenticator = new Authenticator(sessionStorage, { + throwOnError: true, +}); + +const linkedinStrategy = new LinkedinStrategy( + { + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: "https://example.com/auth/linkedin/callback"; + }, + async ({accessToken, refreshToken, extraParams, profile, context}) => { + /* + profile: + type LinkedinProfile = { + id: string; + displayName: string; + name: { + givenName: string; + familyName: string; + }; + emails: Array<{ value: string }>; + photos: Array<{ value: string }>; + _json: LiteProfileData & EmailData; + } & OAuth2Profile; + */ + + // Get the user data from your DB or API using the tokens and profile + return User.findOrCreate({ email: profile.emails[0].value }); + } +); + +authenticator.use(linkedinStrategy, 'linkedin'); +``` + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```tsx copy filename="app/routes/auth/linkedin.tsx" +import { ActionFunction, LoaderFunction } from "remix"; +import { authenticator } from "~/linkedin.server"; + +export let loader: LoaderFunction = () => redirect("/login"); +export let action: ActionFunction = ({ request }) => { + return authenticator.authenticate("linkedin", request); +}; +``` + +```tsx copy filename="app/routes/auth/linkedin/callback.tsx" +import { ActionFunction, LoaderFunction } from "remix"; +import { authenticator } from "~/linkedin.server"; + +export let loader: LoaderFunction = ({ request }) => { + return authenticator.authenticate("linkedin", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +
diff --git a/docs/website/pages/strategies/microsoft.mdx b/docs/website/pages/strategies/microsoft.mdx new file mode 100644 index 0000000..d1ac3f8 --- /dev/null +++ b/docs/website/pages/strategies/microsoft.mdx @@ -0,0 +1,130 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Microsoft Strategy + +The Microsoft strategy is used to authenticate users against an account on [Microsoft Active Directory](https://docs.microsoft.com/en-us/azure/active-directory/develop/) using [Remix-Auth](https://github.com/sergiodxa/remix-auth). +This can be a work/school account or a personal Microsoft account, like Skype, Xbox and Outlook.com. It extends the OAuth2Strategy. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Create an OAuth application + +Follow the steps on [the Microsoft documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) to create a new App Registration. You should select **Web** as the platform, configure a **Redirect URI** and add a client secret. + + If you want to support login with both personal Microsoft accounts and school/work accounts, you might need to configure the supported account types by editing the manifest file. Set `signInAudience` value to `MicrosoftADandPersonalMicrosoftAccount` to allow login also with personal accounts. + +Change your redirect URI to `https://example.com/auth/microsoft/callback` or `http://localhost:4200/auth/microsoft/callback` if you run it locally. + +Be sure to copy the client secret, redirect URI, Tenant ID and the Application (client) ID (under Overview) because you will need them later. + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-microsoft remix-auth-oauth2 + ``` + + + ```bash copy + yarn add remix-auth-microsoft remix-auth-oauth2 + ``` + + + ```bash copy + pnpm install remix-auth-microsoft remix-auth-oauth2 + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { Auth0Strategy } from "remix-auth-auth0"; + +// Create an instance of the authenticator, pass a generic with what your +// strategies will return and will be stored in the session +export const authenticator = new Authenticator(sessionStorage); + +let auth0Strategy = new Auth0Strategy( + { + callbackURL: "https://example.com/auth/auth0/callback", + clientID: "YOUR_AUTH0_CLIENT_ID", + clientSecret: "YOUR_AUTH0_CLIENT_SECRET", + domain: "YOUR_TENANT.us.auth0.com", + }, + async ({ accessToken, refreshToken, extraParams, profile }) => { + // Get the user data from your DB or API using the tokens and profile + return User.findOrCreate({ email: profile.emails[0].value }); + } +); + +authenticator.use(auth0Strategy); +``` + +See [Microsoft docs](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) for more information on `scope` and `prompt` parameters. + +### Applications with single-tenant authentication (no multitenant allowed) + + + +If you want to allow login only for users from a single organization, you should add the `tenantId` attribute to the configuration passed to `MicrosoftStrategy`. +The value of `tenantId` should be the **Directory (tenant) ID** found under **Overview** in your App Registration page. + +You must also select **Accounts in this organizational directory** as Supported account types in your App Registration. + + + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```tsx copy filename="app/routes/auth/microsoft.tsx" +import type { ActionArgs } from "@remix-run/node"; +import { authenticator } from "~/auth.server"; +import { redirect } from "@remix-run/node"; + +export const loader = () => redirect("/login"); + +export const action = ({ request }: ActionArgs) => { + return authenticator.authenticate("microsoft", request); +}; +``` + +```tsx copy filename="app/routes/auth/microsoft/callback.tsx" +import type { LoaderArgs } from "@remix-run/node"; +import { authenticator } from "~/auth.server"; + +export const loader = ({ request }: LoaderArgs) => { + return authenticator.authenticate("microsoft", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +
diff --git a/docs/website/pages/strategies/notion.mdx b/docs/website/pages/strategies/notion.mdx new file mode 100644 index 0000000..d52541a --- /dev/null +++ b/docs/website/pages/strategies/notion.mdx @@ -0,0 +1,90 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Notion Strategy + +Allow users to login with Notion. + +Setup your application at [Notion integrations](https://www.notion.so/my-integrations) +The integration should be setup as **Public integration** + +Copy **OAuth client ID** and **OAuth client secret** to your `NotionStrategy` setup and setup the **Redirect URI**. + +**Notion requires that the redirect URI uses HTTPS**. In development you can use a service like [ngrok](https://ngrok.com/) to be able to test the integration. + + +NPM Package not availabe! Please clone [Github Repository](https://github.com/juhanakristian/remix-auth-notion) + + + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +let notionStrategy = new NotionStrategy( + { + clientID: "", + clientSecret: "", + callbackURL: "https://domain-name.com/auth/notion/callback", + }, + async ({ accessToken, extraParams, profile }) => { + return { + accessToken, + id: profile.id, + name: profile.name, + }; + } +); + +authenticator.use(notionStrategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/auth/notion.tsx" +import { ActionFunction, LoaderFunction, redirect } from "remix"; +import { authenticator } from "~/auth.server"; + +export let loader: LoaderFunction = () => redirect("/login"); + +export let action: ActionFunction = ({ request }) => { + return authenticator.authenticate("notion", request); +}; +``` + +```tsx copy filename="app/routes/auth/notion/callback.tsx" +import { ActionFunction, LoaderFunction, redirect } from "remix"; +import { authenticator } from "~/auth.server"; + +export let loader: LoaderFunction = async ({ request }) => { + return authenticator.authenticate("notion", request, { + successRedirect: "/success", + failureRedirect: "/login", + }); +}; +``` + +Now you can direct the user to login by making a Form with POST to /auth/notion + +```tsx copy filename="app/routes/auth/notion.ts" +import { Form } from "remix"; + +export default function Index() { + return ( +
+ +
+ ); +} +``` + +
diff --git a/docs/website/pages/strategies/okta.mdx b/docs/website/pages/strategies/okta.mdx new file mode 100644 index 0000000..5defc8e --- /dev/null +++ b/docs/website/pages/strategies/okta.mdx @@ -0,0 +1,189 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Okta Strategy + +The Okta strategy is used to authenticate users against an okta account. It extends the OAuth2Strategy. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Create an Okta Web app + +Follow the steps on [the Okta documentation](https://developer.okta.com/docs/guides/sign-into-web-app/nodeexpress/main/#understand-the-callback-route) to create Okta web app and get client ID, client secret and issuer. + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-okta + ``` + + + ```bash copy + yarn add remix-auth-okta + ``` + + + ```bash copy + pnpm install remix-auth-okta + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { OktaStrategy } from "remix-auth-okta"; + +// Create an instance of the authenticator, pass a generic with what your +// strategies will return and will be stored in the session +export const authenticator = new Authenticator(sessionStorage); + +let oktaStrategy = new OktaStrategy( + { + // example of issuer: https://dev-1234.okta.com/oauth2/default + issuer: "YOUR_OKTA_ISSUER", + clientID: "YOUR_OKTA_CLIENT_ID", + clientSecret: "YOUR_OKTA_CLIENT_SECRET", + callbackURL: "https://your-app-domain.com/auth/okta/callback", + }, + async ({ accessToken, refreshToken, extraParams, profile }) => { + // Get the user data from your DB or API using the tokens and profile + return User.findOrCreate({ email: profile.email }); + } +); + +authenticator.use(oktaStrategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```tsx copy filename="app/routes/auth/okta.tsx" +import type { ActionFunction, LoaderFunction } from "remix"; + +import { authenticator } from "~/utils/auth.server"; + +export let loader: LoaderFunction = () => redirect("/login"); + +export let action: ActionFunction = ({ request }) => { + return authenticator.authenticate("okta", request); +}; +``` + +```tsx copy filename="app/routes/auth/okta/callback.tsx" +import type { ActionFunction, LoaderFunction } from "remix"; + +import { authenticator } from "~/utils/auth.server"; + +export let loader: LoaderFunction = ({ request }) => { + return authenticator.authenticate("okta", request, { + successRedirect: "/private", + failureRedirect: "/login", + }); +}; +``` + +
+ +## How to use with custom login page + + + +### Create the strategy instance + +```tsx copy filename="app/utils/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { OktaStrategy } from "remix-auth-okta"; + +// Create an instance of the authenticator, pass a generic with what your +// strategies will return and will be stored in the session +export const authenticator = new Authenticator(sessionStorage); + +let oktaStrategy = new OktaStrategy( + { + // example of issuer: https://dev-1234.okta.com/oauth2/default + issuer: "YOUR_OKTA_ISSUER", + clientID: "YOUR_OKTA_CLIENT_ID", + clientSecret: "YOUR_OKTA_CLIENT_SECRET", + callbackURL: "https://your-app-domain.com/auth/okta/callback", + + // Add this to options for custom login form + withCustomLoginForm: true, + // example of okta domain: https://dev-1234.okta.com + oktaDomain: "YOUR_OKTA_DOMAIN", + }, + async ({ accessToken, refreshToken, extraParams, profile }) => { + // Get the user data from your DB or API using the tokens and profile + return User.findOrCreate({ email: profile.email }); + } +); + +authenticator.use(oktaStrategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ + + +
+ ); +} +``` + +```tsx copy filename="app/routes/auth/okta.tsx" +import type { ActionFunction, LoaderFunction } from "remix"; + +import { authenticator } from "~/utils/auth.server"; + +export let loader: LoaderFunction = () => redirect("/login"); + +export let action: ActionFunction = ({ request }) => { + return authenticator.authenticate("okta", request); +}; +``` + +```tsx copy filename="app/routes/auth/okta/callback.tsx" +import type { ActionFunction, LoaderFunction } from "remix"; + +import { authenticator } from "~/utils/auth.server"; + +export let loader: LoaderFunction = ({ request }) => { + return authenticator.authenticate("okta", request, { + successRedirect: "/private", + failureRedirect: "/login", + }); +}; +``` + +
diff --git a/docs/website/pages/strategies/otp.mdx b/docs/website/pages/strategies/otp.mdx new file mode 100644 index 0000000..0840524 --- /dev/null +++ b/docs/website/pages/strategies/otp.mdx @@ -0,0 +1,683 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# One-Time Password Strategy + +A **One-Time Password Authentication** _Strategy_ for Remix Auth. + +### Features + +- **😌 Easy to Setup**. The Strategy handles the entire authentication flow for you. +- **🔐 Secure**. Encrypted with single-use codes. +- **📧 Magic Link Built-In**. Authenticate your users with a simple click. +- **📚 One Source of Truth**. The database of your choice. +- **🛡 Bulletproof**. Written in strict TypeScript with a high test coverage. +- **🚀 Built on top of Remix Auth**. An amazing authentication library for Remix. + +## Live Demo + +We've created a simple template demo that displays the authentication workflow. Feel free to test it [here](https://otp-stack.fly.dev). + +[![Remix Auth OTP Stack](https://raw.githubusercontent.com/dev-xo/dev-xo/main/remix-auth-otp/assets/images/Thumbnail.png)](https://otp-stack.fly.dev) + + + +The Strategy uses a password-less authentication flow based on email-code validation.
+ +The user will receive an email with a code that can be used to authenticate itself. The code has just a single use and it's valid for a short period of time, which makes it secure and reliable.
+ +Let's see how we can implement this Strategy for our Remix App. + +
+ +## Setup Guide + + + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-otp + ``` + + + ```bash copy + yarn add remix-auth-otp + ``` + + + ```bash copy + pnpm install remix-auth-otp + ``` + + + +{/* prettier-ignore-end */} + +### Databse + +We'll require a database to store our codes. The OTP model has no relations with any other model, this simplifies the process of generating the codes and makes it easier to be implemented into any database of your choice. + +In this example we'll use Prisma ORM with a SQLite database. As long as your database model looks like the following one, you are good to go. + +```ts +// prisma/schema.prisma + +// The model only requires 3 fields: code, active and attempts. +// ... +// The `code` field should be a String. +// The `active` field should be a Boolean and be set to false by default. +// The `attempts` field should be an Int (Number) and be set to 0 by default. +// ... +// The `createdAt` and `updatedAt` fields are optional, and not required. + +model Otp { + id String @id @default(cuid()) + + code String @unique + active Boolean @default(false) + attempts Int @default(0) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +### Email Service + +We'll require an Email Service to send the codes to our users. Feel free to use any service of your choice like [Mailgun](https://www.mailgun.com/), [Sendgrid](https://sendgrid.com/), [Mailchimp](https://mailchimp.com/), etc. + +The goal is to have a sender function similar to the following one. + +```ts +// app/services/email.server.ts +export interface SendEmailBody { + sender: { + name: string; + email: string; + }; + to: { + name?: string; + email: string; + }[]; + subject: string; + htmlContent: string; +} + +export async function sendEmail(body: SendEmailBody) { + return fetch(`https://any-email-service.com`, { + method: "post", + headers: { + Accept: "application/json", + "Api-Key": process.env.EMAIL_PROVIDER_API_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ ...body }), + }); +} +``` + +### Creating the Strategy Instance. + +Create a file called `auth.server.ts` wherever you want.
+Implement the following code and replace the `secret` property with a strong string into your `.env` file. + +```ts copy filename="app/services/auth.server.ts" +import type { User } from "@prisma/client"; + +import { Authenticator } from "remix-auth"; +import { OTPStrategy } from "remix-auth-otp"; + +import { sessionStorage } from "./session.server"; +import { sendEmail } from "./email.server"; +import { db } from "~/db"; + +export let authenticator = new Authenticator(sessionStorage, { + throwOnError: true, +}); + +authenticator.use( + new OTPStrategy( + { + secret: "STRONG_SECRET", + storeCode: async (code) => {}, + sendCode: async ({ email, code, magicLink, user, form, request }) => {}, + validateCode: async (code) => {}, + invalidateCode: async (code, active, attempts) => {}, + }, + async ({ email, code, form, magicLink, request }) => {} + ) +); +``` + +> **Note** +> You can specify how long a session should last by passing a `maxAge` value in milliseconds. Default value is `undefined`, which will not persist the session across browsers restarts. + +### Setting Up the Strategy Options. + +The Strategy Instance requires the following methods: `storeCode`, `sendCode`, `validateCode` and `invalidateCode`. It's important to note that all of them are required. + +> Each of these functions can be extracted to a separate file, but for the sake of simplicity, we'll keep them in the same one. + +```ts copy filename="app/services/auth.server.ts" +// app/services/auth.server.ts +authenticator.use( + new OTPStrategy({ + // Store encrypted code in database. + // It should return a Promise. + storeCode: async (code) => { + await db.otp.create({ + data: { + code: code, + active: true, + attempts: 0 + }, + }) + }, + + // Send code to the user. + // It should return a Promise. + sendCode: async ({ email, code, magicLink, user, form, request }) => { + const sender = { name: 'Remix Auth', email: 'localhost@example.com' } + const to = [{ email }] + const subject = `Here's your OTP Code.` + const htmlContent = ` + + + + + + +

Code: ${code}

+ ${magicLink && `

Alternatively, you can click the Magic Link: ${magicLink}

`} + + + ` + + // Call provider sender email function. + await sendEmail({ sender, to, subject, htmlContent }) + }, + + // Validate code. + // It should return a Promise<{code: string, active: boolean, attempts: number}>. + validateCode: async (code) => { + const otp = await db.otp.findUnique({ + where: { + code: code, + }, + }) + if (!otp) throw new Error('OTP code not found.') + + return { + code: otp.code, + active: otp.active, + attempts: otp.attempts, + } + }, + + // Invalidate code. + // It should return a Promise. + invalidateCode: async (code, active, attempts) => { + if (!active) { + await prisma.otp.delete({ + where: { + code: code + } + }) + } else { + await db.otp.update({ + where: { + code: code, + }, + data: { + active: active, + attempts: attempts, + }, + }) + } + }, + async ({ email, code, magicLink, form, request }) => {}, + }), +) +``` + +All of this CRUD methods should be replaced and adapted with the ones provided by your database. + +### Creating the User and returning it. + +The Strategy has a verify function that will be called before authenticating the user. This should return the user data that will be stored in Session. + +```ts copy filename="app/services/auth.server.ts" +authenticator.use( + new OTPStrategy( + { + // We've already set up this options. + // secret: 'STRONG_SECRET', + // storeCode: async (code) => {}, + // ... + }, + async ({ email, code, magicLink, form, request }) => { + // You can determine whether the user is authenticating + // via OTP submission or Magic Link and run your own logic. + if (form) console.log("Optional form submission logic."); + if (magicLink) console.log("Optional magic link submission logic."); + + // Get user from database. + let user = await db.user.findFirst({ + where: { email }, + }); + + // Create new user. + if (!user) { + user = await db.user.create({ + data: { email }, + }); + } + + // Return user as Session. + return user; + } + ) +); +``` + +And that's it! Feel free to check the [Example Code](https://github.com/dev-xo/remix-auth-otp-stack) implementation in case you wanna use it as a reference. + +### Setup your routes + +Last but not least, we'll require to create the routes that will handle the authentication flow. +Create the following files inside the `app/routes` folder. + +### `login.tsx` + +```tsx copy filename="app/routes/login.tsx" +import type { DataFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { Form, useLoaderData } from "@remix-run/react"; + +import { authenticator } from "~/services/auth.server"; +import { getSession, commitSession } from "~/services/session.server"; + +export async function loader({ request }: DataFunctionArgs) { + const user = await authenticator.isAuthenticated(request, { + successRedirect: "/account", + }); + + const session = await getSession(request.headers.get("Cookie")); + const hasSentEmail = session.has("auth:otp"); + + const email = session.get("auth:email"); + const error = session.get(authenticator.sessionErrorKey); + + // Commits Session to clear any possible error message. + return json( + { user, hasSentEmail, email, error }, + { + headers: { + "Set-Cookie": await commitSession(session), + }, + } + ); +} + +export async function action({ request }: DataFunctionArgs) { + await authenticator.authenticate("OTP", request, { + // Setting `successRedirect` it's required. + // ... + // User is not authenticated yet. + // We want to redirect to the verify code form. (/verify-code or any other route) + successRedirect: "/login", + + // Setting `failureRedirect` it's required. + // ... + // We want to display any possible error message. + // Otherwise the ErrorBoundary / CatchBoundary will be triggered. + failureRedirect: "/login", + }); +} + +export default function Login() { + let { user, hasSentEmail, email, error } = useLoaderData(); + + return ( +
+ {/* Renders any possible error messages. */} + {error && Error: {error.message}} + + {/* Renders the form that sends the email. */} + {!user && !hasSentEmail && ( +
+ + + + +
+ )} + + {/* Renders the form that verifies the code. */} + {hasSentEmail && ( +
+
+ + + + +
+ + {/* Renders the form that requests a new code. */} + {/* Email input is not required, the email is already in Session. */} +
+ +
+
+ )} +
+ ); +} +``` + +### `account.tsx` + +```tsx copy filename="app/routes/account.tsx" +import type { DataFunctionArgs } from "@remix-run/node"; + +import { json } from "@remix-run/node"; +import { Form, useLoaderData } from "@remix-run/react"; +import { authenticator } from "~/services/auth.server"; + +export async function loader({ request }: DataFunctionArgs) { + const user = await authenticator.isAuthenticated(request, { + failureRedirect: "/", + }); + + return json({ user }); +} + +export default function Account() { + let { user } = useLoaderData(); + + return ( +
+

{user ? `Welcome ${user.email}` : "Authenticate"}

+ +
+ +
+
+ ); +} +``` + +### `magic-link.tsx` + +```tsx copy filename="app/routes/magic-link.tsx" +import type { DataFunctionArgs } from "@remix-run/node"; +import { authenticator } from "~/services/auth.server"; + +export async function loader({ request }: DataFunctionArgs) { + await authenticator.authenticate("OTP", request, { + successRedirect: "/account", + failureRedirect: "/login", + }); +} +``` + +### `logout.tsx` + +```tsx copy filename="app/routes/logout.tsx" +import type { DataFunctionArgs } from "@remix-run/node"; +import { authenticator } from "~/services/auth.server"; + +export async function action({ request }: DataFunctionArgs) { + return await authenticator.logout(request, { redirectTo: "/" }); +} +``` + +Done! 🎉 You can now start your server and test the authentication flow. + +
+ +## Options and Customization + +The Strategy includes a few options that can be customized. + +### Email Validation + +The email validation function will validate every email against the regular expression `/.+@.+/`.
+You can customize it by passing a function called `validateEmail` to the OTPStrategy Instance. + +This can be used to verify that the provided email is not a disposable one. + +```ts copy +authenticator.use( + new OTPStrategy({ + validateEmail: async (email) => { + // Handles email validation. + }, + // storeCode: async (code) => {}, + // sendCode: async ({ email, ... }) => {}, + // ... + }) +); +``` + +### Code Generation + +The Code output can be customized by passing an Object called `codeGeneration` to the OTPStrategy Instance. + +Here are its available options: + +```ts copy +/** + * The code generation configuration. + */ +export interface CodeGenerationOptions { + /** + * How long the OTP code will be valid. + * @default 900000 Default is 15 minutes in milliseconds. (1000 * 60 * 15) + */ + expiresAt?: number; + + /** + * How many times an invalid OTP code can be inputted. + * @default 3 + */ + maxAttempts?: number; + + /** + * How long the OTP code will be in length. + * @default 6 + */ + length?: number; + + /** + * Whether the OTP code should contain digits. + * @default false + */ + digits?: boolean; + + /** + * Whether the OTP code should contain lower case alphabets. + * @default false + */ + lowerCaseAlphabets?: boolean; + + /** + * Whether the OTP code should contain upper case alphabets. + * @default true + */ + upperCaseAlphabets?: boolean; + + /** + * Whether the OTP code should contain special characters. + * @default false + */ + specialChars?: boolean; +} + +authenticator.use( + new OTPStrategy({ + codeGeneration: { + length: 12, + expiresAt: 1000 * 60 * 5, // 5 minutes in milliseconds. + // ... other options. + }, + // storeCode: async (code) => {}, + // sendCode: async ({ email, ... }) => {}, + }) +); +``` + +### Magic Link Generation + +The Magic Link is optional and enabled by default. You can decide to opt-out by setting the `enabled` option to `false`. + +Furthermore, the Magic Link can be customized via the `magicLinkGeneration` object in the OTPStrategy instance. +The link generated will be in the format of `https://{baseUrl}{callbackPath}?{codeField}=`. + +```ts copy +/** + * The Magic Link configuration. + */ +export interface MagicLinkGenerationOptions { + /** + * Whether to enable the Magic Link feature. + * @default true + */ + enabled?: boolean; + + /** + * The base URL for building the Magic Link URL. + * If omitted, the `baseUrl` will be inferred from the request. + * @default undefined + */ + baseUrl?: string; + + /** + * The path for the Magic Link callback. + * If your provider route name is different than `magic-link`, you can update it on here. + * @default '/magic-link' + */ + callbackPath?: string; +} +``` + +> **Note:** Just enabling the Magic Link feature is not enough, you will need to also [create the `magic-link` route](#3-magic-link-route). + +### Custom Error Messages + +You can customize the error messages by passing an object called `customErrors` to the OTPStrategy Instance. +This can be useful if you want to translate the error messages to your preferred language, or to give more context to the user. + +```ts copy +/** + * The custom errors configuration. + */ +export interface CustomErrorsOptions { + /** + * The error message when the email address is required. + */ + requiredEmail?: string; + + /** + * The error message when the email address is invalid. + */ + invalidEmail?: string; + + /** + * The error message when the email address is no longer active. + */ + inactiveCode?: string; + + /** + * The error message when the OTP code has expired. + */ + expiredCode?: string; + + /** + * The error message when the OTP code attempts has reached the maximum. + */ + maxCodeAttemptsReached?: string; +} + +authenticator.use( + new OTPStrategy({ + customErrors: { + requiredEmail: "Custom error message for required email.", + }, + // storeCode: async (code) => {}, + // sendCode: async ({ email, ... }) => {}, + }) +); +``` + +### More Options + +The Strategy supports a few more optional configuration options you can set.
+ +```ts copy +/** + * Declares the Strategy configuration + * needed for the developer to correctly work with. + */ +export interface OTPStrategyOptions { + /** + * A secret string used to encrypt and decrypt the OTP code. + * @default '' + */ + secret?: string; + + /** + * The form input name used to get the email address. + * @default "email" + */ + emailField?: string; + + /** + * The form input name used to get the OTP code. + * @default "code" + */ + codeField?: string; + + /** + * The maximum age of the session in milliseconds. ("Remember Me" feature) + * @default undefined + */ + maxAge?: number; + + /** + * A Session key that stores the email address. + * @default "auth:email" + */ + sessionEmailKey?: string; + + /** + * A Session key that stores the encrypted OTP code. + * @default "auth:code" + */ + sessionOtpKey?: string; +} +``` + +## Support + +If you find this module useful, support it with a [Star ⭐](https://github.com/dev-xo/remix-auth-otp)
+It helps the repository grow and gives me motivation to keep working on it. Thank you! + +### Acknowledgments + +Big thanks to [@w00fz](https://github.com/w00fz) for its amazing implementation of the Magic Link feature! + +## License + +Licensed under the [MIT license](https://github.com/dev-xo/stripe-stack/blob/main/LICENSE). diff --git a/docs/website/pages/strategies/spotify.mdx b/docs/website/pages/strategies/spotify.mdx new file mode 100644 index 0000000..7b7397e --- /dev/null +++ b/docs/website/pages/strategies/spotify.mdx @@ -0,0 +1,206 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Spotify Strategy + +The Spotify strategy is used to authenticate users against a Spotify account. It extends the OAuth2Strategy. + +The strategy supports refreshing expired access tokens. The logic for this is ~~stolen from~~ heavily inspired by [remix-auth-supabase](https://github.com/mitchelvanbever/remix-auth-supabase) – thanks [@mitchelvanbever](https://github.com/mitchelvanbever)! + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Create application in Spotify developer dashboard + +1. Go to the [Spotify developer dashboard](https://developer.spotify.com/dashboard/applications) and sign in with your account +2. Click `Create an app` and give the application a suitable name and description +3. Click `Edit settings` and add + - `http://localhost:3000` under `Website` + - `http://localhost:3000/auth/spotify/callback` under `Redirect URIs` (remember to hit `Add`) +4. Hit `Save` at the bottom of the modal +5. Grab your client ID and secret from the dashboard overview, and save them as env variables + +Remember to update `Website` and `Redirect URIs` if/when deploying your app. + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-spotify + ``` + + + ```bash copy + yarn add remix-auth-spotify + ``` + + + ```bash copy + pnpm install remix-auth-spotify + ``` + + + +{/* prettier-ignore-end */} + +### Setup ENV variables + +```bash +# .env +SPOTIFY_CLIENT_ID="your client id" +SPOTIFY_CLIENT_SECRET="your client secret" +SPOTIFY_CALLBACK_URL="your callback url" # e.g. http://localhost:3000/auth/spotify/callback +``` + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { SpotifyStrategy } from "remix-auth-spotify"; + +import { sessionStorage } from "~/services/session.server"; + +if (!process.env.SPOTIFY_CLIENT_ID) { + throw new Error("Missing SPOTIFY_CLIENT_ID env"); +} + +if (!process.env.SPOTIFY_CLIENT_SECRET) { + throw new Error("Missing SPOTIFY_CLIENT_SECRET env"); +} + +if (!process.env.SPOTIFY_CALLBACK_URL) { + throw new Error("Missing SPOTIFY_CALLBACK_URL env"); +} + +// See https://developer.spotify.com/documentation/general/guides/authorization/scopes +const scopes = ["user-read-email"].join(" "); + +export const spotifyStrategy = new SpotifyStrategy( + { + clientID: process.env.SPOTIFY_CLIENT_ID, + clientSecret: process.env.SPOTIFY_CLIENT_SECRET, + callbackURL: process.env.SPOTIFY_CALLBACK_URL, + sessionStorage, + scope: scopes, + }, + async ({ accessToken, refreshToken, extraParams, profile }) => ({ + accessToken, + refreshToken, + expiresAt: Date.now() + extraParams.expiresIn * 1000, + tokenType: extraParams.tokenType, + user: { + id: profile.id, + email: profile.emails[0].value, + name: profile.displayName, + image: profile.__json.images?.[0]?.url, + }, + }) +); + +export const authenticator = new Authenticator(sessionStorage, { + sessionKey: spotifyStrategy.sessionKey, + sessionErrorKey: spotifyStrategy.sessionErrorKey, +}); + +authenticator.use(spotifyStrategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/auth/spotify.tsx" +import type { ActionArgs } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; + +import { authenticator } from "~/services/auth.server"; + +export function loader() { + return redirect("/login"); +} + +export async function action({ request }: ActionArgs) { + return await authenticator.authenticate("spotify", request); +} +``` + +```tsx copy filename="app/routes/auth/spotify.callback.tsx" +import type { LoaderArgs } from "@remix-run/node"; + +import { authenticator } from "~/services/auth.server"; + +export function loader({ request }: LoaderArgs) { + return authenticator.authenticate("spotify", request, { + successRedirect: "/", + failureRedirect: "/login", + }); +} +``` + +```tsx copy filename="app/routes/logout.tsx" +import type { ActionArgs } from "@remix-run/node"; +import { json, redirect } from "@remix-run/node"; + +import { destroySession, getSession } from "~/services/session.server"; + +export async function action({ request }: ActionArgs) { + return redirect("/", { + headers: { + "Set-Cookie": await destroySession( + await getSession(request.headers.get("cookie")) + ), + }, + }); +} + +export function loader() { + throw json({}, { status: 404 }); +} +``` + +### Use the strategy + +```tsx copy filename="app/routes/index.tsx" +import type { LoaderArgs } from "@remix-run/node"; +import { Form, useLoaderData } from "@remix-run/react"; + +import { spotifyStrategy } from "~/services/auth.server"; + +export async function loader({ request }: LoaderArgs) { + return spotifyStrategy.getSession(request); +} + +export default function Index() { + const data = useLoaderData(); + const user = data?.user; + + return ( +
+

Welcome to Remix!

+

+ Check out the docs to get started. +

+ {user ? ( +

You are logged in as: {user?.email}

+ ) : ( +

You are not logged in yet!

+ )} +
+ +
+
+ ); +} +``` + +
diff --git a/docs/website/pages/strategies/steam.mdx b/docs/website/pages/strategies/steam.mdx new file mode 100644 index 0000000..c3acb10 --- /dev/null +++ b/docs/website/pages/strategies/steam.mdx @@ -0,0 +1,143 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Steam Strategy + +This is a [Steam](https://steamcommunity.com/) strategy for [remix-auth](https://github.com/sergiodxa/remix-auth) library. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ❓ | + +## Setup Guide + + + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-steam + ``` + + + ```bash copy + yarn add remix-auth-steam + ``` + + + ```bash copy + pnpm install remix-auth-steam + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +To properly use the library, you need to maintain these additional files in your app directory: + +```tsx copy filename="app/services/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { sessionStorage } from "~/services/session.server"; +import { SteamStrategy, SteamStrategyVerifyParams } from "remix-auth-steam"; + +export type User = SteamStrategyVerifyParams; + +export let authenticator = new Authenticator(sessionStorage); + +authenticator.use( + new SteamStrategy( + { + returnURL: "http://localhost:3000/auth/steam/callback", + apiKey: "YOUR_STEAM_API_KEY", // you can get it here: https://steamcommunity.com/dev/apikey + }, + async (user) => user // perform additional checks for user here, I just leave this to SteamStrategyVerifyParams value + ) +); +``` + +```tsx copy filename="app/services/session.server.ts" +import { createCookieSessionStorage } from "remix"; + +const calculateExpirationDate = (days: number) => { + const expDate = new Date(); + expDate.setDate(expDate.getDate() + days); + return expDate; +}; + +// export the whole sessionStorage object +export let sessionStorage = createCookieSessionStorage({ + cookie: { + name: "_session", // use any name you want here + sameSite: "lax", // this helps with CSRF + path: "/", // remember to add this so the cookie will work in all routes + httpOnly: true, // for security reasons, make this cookie http only + secrets: ["s3cr3t"], // replace this with an actual secret + secure: process.env.NODE_ENV === "production", // enable this in prod only + expires: calculateExpirationDate(7), // expire cookie after 7 days + }, +}); + +// you can also export the methods individually for your own usage +export let { getSession, commitSession, destroySession } = sessionStorage; +``` + +### Setup your routes + +```tsx copy filename="app/routes/auth/steam.tsx" +import { LoaderFunction } from "remix"; +import { authenticator } from "~/services/auth.server"; + +export let loader: LoaderFunction = async ({ request }) => { + await authenticator.authenticate("steam", request, {}); +}; +``` + +```tsx copy filename="app/routes/auth/steam/callback.tsx" +import { LoaderFunction } from "remix"; +import { authenticator } from "~/services/auth.server"; + +export let loader: LoaderFunction = ({ request }) => { + return authenticator.authenticate("steam", request, { + successRedirect: "/", + failureRedirect: "/login", + }); +}; +``` + + + +## Utilization + +After that, navigate to `localhost:3000/auth/steam` to check if it works. Here is an example of checking if user is authenticated: `app/routes/index.tsx`: + +```tsx copy filename="app/routes/index.tsx" +import { LoaderFunction, useLoaderData } from "remix"; +import { authenticator, User } from "~/services/auth.server"; + +export let loader: LoaderFunction = async ({ request }) => { + const user = await authenticator.isAuthenticated(request); + return user; +}; + +export default function Index() { + const user: User | null = useLoaderData(); + + return ( +
+ {user ?

User name: {user!.nickname}

:

Not authenticated

} +
+ ); +} +``` + +If you are logged in, you should see your username from Steam API, otherwise you will see Not authenticated message. diff --git a/docs/website/pages/strategies/twillio.mdx b/docs/website/pages/strategies/twillio.mdx new file mode 100644 index 0000000..2bbae64 --- /dev/null +++ b/docs/website/pages/strategies/twillio.mdx @@ -0,0 +1,225 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Twilio Strategy + +Uses the [Twilio Verify API](https://www.twilio.com/verify) to validate users via SMS and add simple phone-based auth to a [Remix](https://remix.run) application using [Remix Auth](https://github.com/sergiodxa/remix-auth). + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Create an OAuth application + +First you need to create a new application in the [Linkedin's developers page](https://developer.linkedin.com/). Then I encourage you to read [this documentation page](https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?tabs=HTTPS#prerequisites), it explains how to configure your app and gives you useful information on the auth flow. The app is mandatory in order to obtain a `clientID` and `client secret` to use with the Linkedin's API. + +This library is designed to require as little config as possible. There's no need to generate your own codes, validate input, or store anything aside from the user's phone number in your database. If you need more advanced functionality and customizations, check out [remix-auth-otp](https://github.com/dev-xo/remix-auth-otp). + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-twilio + ``` + + + ```bash copy + yarn add remix-auth-twilio + ``` + + + ```bash copy + pnpm install remix-auth-twilio + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { TwilioStrategy } from "remix-auth-twilio"; +import { sessionStorage } from "./session.server"; +import { User, findOrCreateUser } from "your-db-client"; + +export let authenticator = new Authenticator(sessionStorage); + +const twilioStrategy = new TwilioStrategy( + { + accountSID: "YOUR_ACCOUNT_SID", + authToken: "YOUR_AUTH_TOKEN", + serviceSID: "YOUR_SERVICE_SID", + }, + async ({ phone, formData, request }) => { + // The user has been authenticated through Twilio. + // Get the user data from your DB or API using the formatted phone number. + return findOrCreateUser({ phone }); + } +); + +authenticator.use(twilioStrategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +import { ActionArgs, DataFunctionArgs, json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { authenticator } from "../auth.server"; +import { sessionStorage } from "../session.server"; + +export async function action({ request }: ActionArgs) { + const formData = await request.clone().formData(); + + return authenticator.authenticate("twilio", request, { + successRedirect: formData.has("code") ? "/account" : "/login", + failureRedirect: "/login", + }); +} + +export async function loader({ request }: DataFunctionArgs) { + await authenticator.isAuthenticated(request, { + successRedirect: "/account", + }); + + const session = await sessionStorage.getSession( + request.headers.get("Cookie") + ); + + const phone = session.get("twilio:phone") ?? null; + + const error = session.get(authenticator.sessionErrorKey); + + return json( + { phone, error }, + { + headers: { + "Set-Cookie": await sessionStorage.commitSession(session), + }, + } + ); +} + +export default function LoginPage() { + const { phone, error } = useLoaderData(); + + return ( +
+ {error &&

Error: {error.message}

} + {phone ? ( + <> + + + + + ) : ( + <> + + + + )} +
+ ); +} +``` + +```tsx copy filename="app/routes/account.tsx" +import { DataFunctionArgs, json } from "@remix-run/node"; +import { Link, useLoaderData } from "@remix-run/react"; +import { authenticator } from "~/auth.server"; + +export async function loader({ request }: DataFunctionArgs) { + const user = await authenticator.isAuthenticated(request, { + failureRedirect: "/login", + }); + + return json({ user }); +} + +export default function AccountPage() { + const { user } = useLoaderData(); + + return ( +
+

Hello, {user.phone}

+

+ Log out +

+
+ ); +} +``` + +```tsx copy filename="app/routes/logout.tsx" +import { DataFunctionArgs } from "@remix-run/node"; +import { authenticator } from "~/auth.server"; + +export async function loader({ request }: DataFunctionArgs) { + await authenticator.logout(request, { redirectTo: "/login" }); +} + +export default function LogoutPage() { + return null; +} +``` + +
+ +## Options + +```ts +type TwilioStrategyOptions = { + /** + * Twilio Account SID + */ + accountSID: string; + /** + * Twilio Auth Token + */ + authToken: string; + /** + * Twilio Verify Service SID + */ + serviceSID: string; + /** + * A function that sends a verification code to the user. + */ + sendCode?: ({ phone }: { phone: string }) => Promise; + /** + * A function that validates the verification code provided by the user. + */ + validateCode?: ({ + code, + phone, + }: { + code: string; + phone: string; + }) => Promise; + /** + * A function that formats the phone number provided by the user. + * This library uses the `phone` package to validate phone numbers. + * You can optionally provide your own validation function here. + */ + formatPhoneNumber?: (phone: string) => string; +}; +``` diff --git a/docs/website/pages/strategies/twitch.mdx b/docs/website/pages/strategies/twitch.mdx new file mode 100644 index 0000000..625bda3 --- /dev/null +++ b/docs/website/pages/strategies/twitch.mdx @@ -0,0 +1,115 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Twitch Strategy + +The Twitch strategy is used to authenticate users against a Twitch account. It extends the OAuth2Strategy. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + + +### Create an OAuth application + +Follow the steps on [the Twitch documentation](https://dev.twitch.tv/docs/authentication/register-app) to create a new application and get a client ID and secret. + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install @03gibbss/remix-auth-twitch + ``` + + + ```bash copy + yarn add @03gibbss/remix-auth-twitch + ``` + + + ```bash copy + pnpm install @03gibbss/remix-auth-twitch + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { TwitchStrategy } from "@03gibbss/remix-auth-twitch"; + +let twitchStrategy = new TwitchStrategy( + { + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: "https://example.com/auth/twitch/callback", + }, + async ({ accessToken, extraParams, profile }) => { + // Get the user data from your DB or API using the tokens and profile + const user = await db.user.findUnique({ + where: { twitchId: profile.id }, + }); + + if (user) { + return { id: user.id, displayName: user.displayName }; + } + + const newUser = await db.user.create({ + data: { twitchId: profile.id, displayName: profile.display_name }, + }); + + return { id: newUser.id, displayName: newUser.displayName }; + } +); + +authenticator.use(twitchStrategy); +``` + +### Setup your routes + +```tsx copy filename="app/routes/login.tsx" +export default function Login() { + return ( +
+ +
+ ); +} +``` + +```tsx copy filename="app/routes/auth/twitch.tsx" +import { ActionFunction, LoaderFunction, redirect } from "remix"; +import { authenticator } from "~/auth.server"; + +export let loader: LoaderFunction = () => redirect("/login"); + +export let action: ActionFunction = ({ request }) => { + return authenticator.authenticate("twitch", request); +}; +``` + +```tsx copy filename="app/routes/auth/twitch/callback.tsx" +import { LoaderFunction } from "remix"; +import { authenticator } from "~/auth.server"; + +export let loader: LoaderFunction = ({ request }) => { + return authenticator.authenticate("twitch", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +}; +``` + +
diff --git a/docs/website/pages/strategies/twitter.mdx b/docs/website/pages/strategies/twitter.mdx new file mode 100644 index 0000000..ca52c1c --- /dev/null +++ b/docs/website/pages/strategies/twitter.mdx @@ -0,0 +1,137 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# Twitter Strategy + +Remix Auth plugin for Twitter OAuth 1.0a. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ✅ | + +## Setup Guide + + +### Prerequisites +* Your app is registered to Twitter and has consumer key and secret issued https://developer.twitter.com/en/docs/authentication/oauth-1-0a/api-key-and-secret +* Your app has [remix-auth](https://github.com/sergiodxa/remix-auth) set up and `authenticator` is provided: + ```typescript + // app/services/auth.server.ts + export let authenticator = ...; + ``` + +### Install the package + +import { Tab, Tabs } from "nextra-theme-docs"; + +{/* prettier-ignore-start */} + + + + ```bash copy + npm install remix-auth-twitter + ``` + + + ```bash copy + yarn add remix-auth-twitter + ``` + + + ```bash copy + pnpm install remix-auth-twitter + ``` + + + +{/* prettier-ignore-end */} + +### Create the strategy instance + +```tsx copy filename="app/services/auth.server.ts" +import { Authenticator } from "remix-auth"; +import { sessionStorage } from "~/services/session.server"; +import { TwitterStrategy } from "remix-auth-twitter"; + +export let authenticator = new Authenticator(sessionStorage); + +const clientID = process.env.TWITTER_CONSUMER_KEY; +const clientSecret = process.env.TWITTER_CONSUMER_SECRET; +if (!clientID || !clientSecret) { + throw new Error( + "TWITTER_CONSUMER_KEY and TWITTER_CONSUMER_SECRET must be provided" + ); +} + +authenticator.use( + new TwitterStrategy( + { + clientID, + clientSecret, + callbackURL: "https://my-app/login/callback", + alwaysReauthorize: false, // otherwise, ask for permission every time + }, + // Define what to do when the user is authenticated + async ({ accessToken, accessTokenSecret, profile }) => { + // profile contains userId and screenName + + // Return a user object to store in sessionStorage. + // You can also throw Error to reject the login + return await registerUser(accessToken, accessTokenSecret, profile); + } + ), + // each strategy has a name and can be changed to use another one + // same strategy multiple times, especially useful for the OAuth2 strategy. + "twitter" +); +``` + +### Setup your routes + +Follow the [remix-auth docs](https://github.com/sergiodxa/remix-auth#readme) to set up logout flow and `isAuthenticated`. + +The log in flow would look like this: + +1. User comes to "login" page (e.g. `/login`). +2. The app will redirect user to Twitter's auth page. +3. Once user finishes auth, Twitter will redirect user back to your app (e.g. `/login/callback`). +4. The app will verify the user and let the user login. + +To set up the login flow, follow the code below: + +```tsx copy filename="app/routes/login.tsx" +import { ActionFunction } from "remix"; +import { authenticator } from "~/services/auth.server"; + +// Normally this will redirect user to twitter auth page +export let action: ActionFunction = async ({ request }) => { + await authenticator.authenticate("twitter", request, { + successRedirect: "/dashboard", // Destination in case the user is already logged in + }); +}; +``` + +```tsx copy filename="app/routes/login.callback.tsx" +import { LoaderFunction } from "remix"; +import { authenticator } from "~/services/auth.server"; + +// This will be called after twitter auth page +export let loader: LoaderFunction = async ({ request }) => { + await authenticator.authenticate("twitter", request, { + successRedirect: "/dashboard", + failureRedirect: "/login/failure", + }); +}; +``` + +Then let the user do `POST /login`: + +```tsx copy +
+ +
+``` + +
diff --git a/docs/website/pages/strategies/web-authn.mdx b/docs/website/pages/strategies/web-authn.mdx new file mode 100644 index 0000000..12cc36c --- /dev/null +++ b/docs/website/pages/strategies/web-authn.mdx @@ -0,0 +1,305 @@ +import { Callout, Steps } from "nextra-theme-docs"; + +# WebAuthn (Passkey) Strategy Strategy + +Authenticate users with [Web Authentication](https://www.w3.org/TR/webauthn-2/) passkeys and physical tokens. It is implemented using [SimpleWebAuthn](https://simplewebauthn.dev) and supports user authentication and user registration using passkeys. + +> This package should be considered unstable. It works in my limited testing, but I haven't covered every case or written automated tests. _Caveat emptor_. + +## Supported runtimes + +| Runtime | Has Support | +| ---------- | ----------- | +| Node.js | ✅ | +| Cloudflare | ❓ | + + +This package also only supports ESM, because package.json is scary and I'm not certain how to set up the necessary build steps. You might need to add this to your `serverDependenciesToBundle` in your remix.config.js file. + + + +## About Web Authentication + +Web Authentication lets a user register a device as a passkey. The device could be a USB device, like a Yubikey, the computer running the webpage, or a separate Bluetooth connected device like a smartphone. [This page has a good summary of the benefits](https://developer.apple.com/passkeys/), and you can [try it firsthand here](https://webauthn.io). + +WebAuthn follows a two-step process. First, a device is _registered_ as a passkey. The browser generates a private/public key pair, associates it with a user ID and username, and sends the public key to the server to be verified. At this point the server could create a new user with that passkey, or if the user is already signed in the server could associate that passkey with the existing user. + +In the _authentication_ step, the browser uses the passkey's private key to sign a challenge sent by the server, which the server checks with its stored public key in the verification step. + +This strategy handles generating the challenge, storing it in session storage, passing the WebAuthn options to the client, generating the passkeys, and verifying the passkeys. Since this strategy requires database persistence and browser-based APIs, it requires a bit more work to set up. + + +This strategy also requires generating string user IDs on the browser. If your setup requires generating IDs, you might have to work around this limitation by creating a mapping of the authenticator userIds and your actual userIds. + + + +## Setup Guide + + +### Database + +```ts copy +interface Authenticator { + // SQL: Encode to base64url then store as `TEXT` or a large `VARCHAR(511)`. Index this column + credentialID: string; + // Some reference to the user object. Consider indexing this column too + userId: string; + // SQL: Encode to base64url and store as `TEXT` + credentialPublicKey: string; + // SQL: Consider `BIGINT` since some authenticators return atomic timestamps as counters + counter: number; + // SQL: `VARCHAR(32)` or similar, longest possible value is currently 12 characters + // Ex: 'singleDevice' | 'multiDevice' + credentialDeviceType: string; + // SQL: `BOOL` or whatever similar type is supported + credentialBackedUp: boolean; + // SQL: `VARCHAR(255)` and store string array or a CSV string + // Ex: ['usb' | 'ble' | 'nfc' | 'internal'] + transports: string; +} +``` + +### Create the strategy instance + +This strategy tries not to make assumptions about your database structure, so it requires several configuration options. + +```ts +authenticator.use( + new WebAuthnStrategy( + { + // The human-readable name of your app + rpName: "Remix Auth WebAuthn", + // The hostname of the website, determines where passkeys can be used + // See https://www.w3.org/TR/webauthn-2/#relying-party-identifier + rpID: env.NODE_ENV === "development" ? "localhost" : env.APP_URL, + // Website URL (or array of URLs) where the registration can occur + origin: env.APP_URL, + // Return the list of authenticators associated with this user. You might + // need to transform a CSV string into a list of strings at this step. + getUserAuthenticators: async (user) => { + const authenticators = await getAuthenticators(user) + + return authenticators.map((authenticator) => ({ + ...authenticator + transports: authenticator.transports.split(",") + })); + }, + // Transform the user object into the shape expected by the strategy. + // You can use a regular username, the users email address, or something else. + getUserDetails: (user) => ({ id: user!.id, username: user!.email }), + // Find a user in the database with their username/email. + getUserByUsername: (username) => getUserByEmail(username), + }, + async function verify({ authenticator, type, username }) { + // ... + } + ) +); +``` + +### Write your verify function + +The verify function handles both the _registration_ and _authentication_ steps, and expects you to return a `user` object or throw an error if verification fails. + +The verify function will receive an Authenticator object (without the userId), the provided username, and the type of verification - either `registration` or `authentication`. + +Note: You'll have to implement your own endpoints for adding additional authenticators to existing users. + +```ts +authenticator.use( + new WebAuthnStrategy( + { + // Options here... + }, + async function verify({ authenticator, type, username }) { + let user: User | null = null; + const savedAuthenticator = await getAuthenticatorById( + authenticator.credentialID + ); + if (type === "registration") { + // Check if the authenticator exists in the database + if (savedAuthenticator) { + throw new Error("Authenticator has already been registered."); + } else { + // Username is null for authentication verification, + // but required for registration verification. + // It is unlikely this error will ever be thrown, + // but it helps with the TypeScript checking + if (!username) throw new Error("Username is required."); + user = await getUserByEmail(username); + + // Don't allow someone to register a passkey for + // someone elses account. + if (user) throw new Error("User already exists."); + + // Create a new user and authenticator + user = await createUser(username); + await createAuthenticator(authenticator, user.id); + } + } else if (type === "authentication") { + if (!savedAuthenticator) throw new Error("Authenticator not found"); + user = await getUserById(savedAuthenticator.userId); + } + + if (!user) throw new Error("User not found"); + return user; + } + ) +); +``` + +### Set up your login page loader and action + +The login page will need a loader to supply the WebAuthn options from the server, and an action to deliver the passkey back to the server. + +```ts +// /app/routes/_auth.login.ts +export let loader = async ({ request }: LoaderArgs) => { + await authenticator.isAuthenticated(request, { successRedirect: "/" }); + + // When we pass a GET request to the authenticator, it will + // throw a response that includes the WebAuthn options and + // stores the challenge on session storage. To avoid needing + // a CatchBoundary, we catch the response here and return it as + // loader data. + try { + await authenticator.authenticate("webauthn", request); + } catch (response) { + if (response instanceof Response && response.status === 200) { + return response; + } + throw response; + } +}; + +export let action = async ({ request }: DataFunctionArgs) => { + // If you're using multiple authenticator strategies, you can + // invoke them here based on the form data that was submitted. + try { + await authenticator.authenticate("webauthn", request, { + successRedirect: "/", + }); + } catch (error) { + // You can catch the error here and resolve the message + // for more direct error handling. + if (error instanceof Response && error.status >= 400) { + return { error: (await error.json()) as { message: string } }; + } + throw error; + } + + return null; +}; +``` + +## Set up the form + +For ease-of-use, this strategy provides an `onSubmit` handler which performs the necessary browser-side actions to generate passkeys. The `onSubmit` handler is generated by passing in the options object from the loader above. Depending on your setup, you might need to implement separate forms for registration and authentication. + +When registering, the process follows a few steps: + +1. The user requests registration by entering their desired username and pressing the button, which submits a GET request to get updated options. +2. The server responds with whether the username is taken and if the user already has registered a passkey so the browser doesn't produce duplicates. +3. The form must be submitted a second time, as POST this time, with the actual passkey for registration. +4. The server verifies the passkey, creates the new user, and logs the user in. + +Your registration form should include a required `username` field and a button for registration. The button should change state and behavior based on whether the options from the loader indicate that the username is available. This is demonstrated below. + +Authentication is a simpler process and only requires one button press: + +1. The user requests authentication, and the browser shows the available passkeys for the domain. +2. The user picks a passkey, and the form is generated and submitted to the server. +3. The server verifies the passkey by checking it against the database, and logs the user in. + +Since the username is stored with the passkey in the browser, the `username` field is not required for the authentication form. + +Two hidden form inputs are required on the form. The one named `response` will have its value dynamically set to the generated passkey after the user goes through the browser's passkey process. The `type` field should be either `registration` or `authentication`, and is also automatically set based on the value attribute of the button which submits the form. + +Here's what the forms might look like in practice: + +```tsx +// /app/routes/_auth.login.ts +export default function Login() { + let actionData = useActionData>>(); + let navigationData = useNavigation(); + let options = useLoaderData(); + + const [usernameAvailable, setUsernameAvailable] = useState( + options.usernameAvailable + ); + useEffect(() => { + setUsernameAvailable(options.usernameAvailable); + }, [options]); + + return ( +
+

Log in to your account.

+
+ {/* These two hidden form inputs are required on both forms */} + + +
+ + setUsernameAvailable(null)} + disabled={navigationData.state === "submitting"} + /> +
+ + + {usernameAvailable === false ? ( +

Username is taken

+ ) : usernameAvailable === true ? ( +

Username available

+ ) : null} +
+
+ {/* These two hidden form inputs are required on both forms */} + + + +
+ {actionData && "error" in actionData ? ( +

{actionData.error?.message}

+ ) : null} +
+ ); +} +``` + +
diff --git a/docs/website/theme.config.tsx b/docs/website/theme.config.tsx new file mode 100644 index 0000000..83dcf44 --- /dev/null +++ b/docs/website/theme.config.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { DocsThemeConfig } from "nextra-theme-docs"; + +const Logo = () => { + return ( +

📀 Remix-Auth

+ ); +}; + +const config: DocsThemeConfig = { + logo: Logo, + project: { + link: "https://github.com/sergiodxa/remix-auth", + }, + chat: { + link: "https://discord.com/invite/xwx7mMzVkA", + }, + docsRepositoryBase: "https://github.com/sergiodxa/remix-auth", + footer: { + text: Logo, + }, + primaryHue: { dark: 165, light: 220 }, +}; + +export default config;