feat: implement turnstile on signup (#13913)

* wip

* feat: turnstile + removal of trpcState

* fix: use react-turnstile

* fix: i18n hydration errors

* wip: server side validation for cf token

* fix: hide in e2e tests

* feat:throw on error of token

* fix: set correct form values onVerify

* fix: use getIp for remoteIp and use correct header for token input

* chore: update .env.example

* fix: Update .env.example

* Update apps/web/pages/signup.tsx

* chore: fix typo

* fix: typo

* feat: hide until error

* feat: appearance interaction only

* remove cloudflare from e2e

* fix: legacyBehavior

* fix links + wait for load

* fix: signup tests

* test: wait for load state

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
sean-brydon
2024-04-04 10:21:06 -03:00
committed by GitHub
co-authored by Keith Williams Udit Takkar
parent 4a173c48b8
commit 89fe383c9f
11 changed files with 243 additions and 102 deletions
+4
View File
@@ -212,6 +212,10 @@ E2E_TEST_MAILHOG_ENABLED=
# **********************************************************************************************************
# Cloudflare Turnstile
NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
CLOUDFLARE_TURNSTILE_SECRET=
# Set the following value to true if you wish to enable Team Impersonation
NEXT_PUBLIC_TEAM_IMPERSONATION=false
+12
View File
@@ -0,0 +1,12 @@
import type { TurnstileProps } from "react-turnstile";
import Turnstile from "react-turnstile";
import { CLOUDFLARE_SITE_ID } from "@calcom/lib/constants";
type Props = Omit<TurnstileProps, "sitekey">;
export default function TurnstileWidget(props: Props) {
if (!CLOUDFLARE_SITE_ID || process.env.NEXT_PUBLIC_IS_E2E) return null;
return <Turnstile {...props} sitekey={CLOUDFLARE_SITE_ID} theme="auto" />;
}
+4 -2
View File
@@ -24,8 +24,10 @@ const querySchema = z.object({
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const emailVerificationEnabled = await getFeatureFlag(prisma, "email-verification");
await ssrInit(ctx);
const signupDisabled = await getFeatureFlag(prisma, "disable-signup");
const ssr = await ssrInit(ctx);
const token = z.string().optional().parse(ctx.query.token);
const redirectUrlData = z
.string()
@@ -42,8 +44,8 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
redirectUrl,
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
isSAMLLoginEnabled,
trpcState: ssr.dehydrate(),
prepopulateFormValues: undefined,
emailVerificationEnabled,
};
// username + email prepopulated from query params
+1
View File
@@ -119,6 +119,7 @@
"react-schemaorg": "^2.0.0",
"react-select": "^5.7.0",
"react-timezone-select": "^1.4.0",
"react-turnstile": "^1.1.3",
"react-use-intercom": "1.5.1",
"remove-markdown": "^0.5.0",
"rrule": "^2.7.1",
+8
View File
@@ -4,8 +4,10 @@ import calcomSignupHandler from "@calcom/feature-auth/signup/handlers/calcomHand
import selfHostedSignupHandler from "@calcom/feature-auth/signup/handlers/selfHostedHandler";
import { type RequestWithUsernameStatus } from "@calcom/features/auth/signup/username";
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
import getIP from "@calcom/lib/getIP";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
import { signupSchema } from "@calcom/prisma/zod-utils";
function ensureSignupIsEnabled(req: RequestWithUsernameStatus) {
@@ -36,8 +38,14 @@ function ensureReqIsPost(req: RequestWithUsernameStatus) {
}
export default async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
const remoteIp = getIP(req);
// Use a try catch instead of returning res every time
try {
await checkCfTurnstileToken({
token: req.headers["cf-access-token"] as string,
remoteIp,
});
ensureReqIsPost(req);
ensureSignupIsEnabled(req);
+32 -7
View File
@@ -3,6 +3,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react";
import { Trans } from "next-i18next";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Script from "next/script";
@@ -16,9 +17,15 @@ import getStripe from "@calcom/app-store/stripepayment/lib/client";
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { classNames } from "@calcom/lib";
import { APP_NAME, URL_PROTOCOL_REGEX, IS_CALCOM, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import {
APP_NAME,
URL_PROTOCOL_REGEX,
IS_CALCOM,
WEBAPP_URL,
WEBSITE_URL,
CLOUDFLARE_SITE_ID,
} from "@calcom/lib/constants";
import { fetchUsername } from "@calcom/lib/fetchUsername";
import { pushGTMEvent } from "@calcom/lib/gtm";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
@@ -45,8 +52,11 @@ import PageWrapper from "@components/PageWrapper";
const signupSchema = apiSignupSchema.extend({
apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API
cfToken: z.string().optional(),
});
const TurnstileCaptcha = dynamic(() => import("@components/auth/Turnstile"), { ssr: false });
type FormValues = z.infer<typeof signupSchema>;
export type SignupProps = inferSSRProps<typeof getServerSideProps>;
@@ -171,6 +181,7 @@ export default function Signup({
isSAMLLoginEnabled,
orgAutoAcceptEmail,
redirectUrl,
emailVerificationEnabled,
}: SignupProps) {
const [premiumUsername, setPremiumUsername] = useState(false);
const [usernameTaken, setUsernameTaken] = useState(false);
@@ -179,7 +190,6 @@ export default function Signup({
const telemetry = useTelemetry();
const { t, i18n } = useLocale();
const router = useRouter();
const flags = useFlagMap();
const formMethods = useForm<FormValues>({
resolver: zodResolver(signupSchema),
defaultValues: prepopulateFormValues satisfies FormValues,
@@ -225,7 +235,8 @@ export default function Signup({
const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username;
const signUp: SubmitHandler<FormValues> = async (data) => {
const signUp: SubmitHandler<FormValues> = async (_data) => {
const { cfToken, ...data } = _data;
await fetch("/api/auth/signup", {
body: JSON.stringify({
...data,
@@ -234,6 +245,7 @@ export default function Signup({
}),
headers: {
"Content-Type": "application/json",
"cf-access-token": cfToken ?? "invalid-token",
},
method: "POST",
})
@@ -243,7 +255,7 @@ export default function Signup({
pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language });
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
const verifyOrGettingStarted = flags["email-verification"] ? "auth/verify-email" : "getting-started";
const verifyOrGettingStarted = emailVerificationEnabled ? "auth/verify-email" : "getting-started";
const callBackUrl = `${
searchParams?.get("callbackUrl")
? isOrgInviteByLink
@@ -362,6 +374,16 @@ export default function Signup({
{...register("password")}
hintErrors={["caplow", "min", "num"]}
/>
{/* Cloudflare Turnstile Captcha */}
{CLOUDFLARE_SITE_ID ? (
<TurnstileCaptcha
appearance="interaction-only"
onVerify={(token) => {
formMethods.setValue("cfToken", token);
}}
/>
) : null}
<CheckboxField
onChange={() => handleConsentChange(COOKIE_CONSENT)}
description={t("cookie_consent_checkbox")}
@@ -375,6 +397,9 @@ export default function Signup({
!!formMethods.formState.errors.email ||
!formMethods.getValues("email") ||
!formMethods.getValues("password") ||
(CLOUDFLARE_SITE_ID &&
!process.env.NEXT_PUBLIC_IS_E2E &&
!formMethods.getValues("cfToken")) ||
isSubmitting ||
usernameTaken
}>
@@ -497,14 +522,14 @@ export default function Signup({
className="text-emphasis hover:underline"
href={`${WEBSITE_URL}/terms`}
target="_blank">
<a>Terms</a>
Terms
</Link>{" "}
and{" "}
<Link
className="text-emphasis hover:underline"
href={`${WEBSITE_URL}/privacy`}
target="_blank">
<a>Privacy Policy</a>
Privacy Policy
</Link>
.
</Trans>
+6 -5
View File
@@ -25,6 +25,7 @@ test.describe("Signup Flow Test", async () => {
});
await page.goto("/signup");
await page.waitForLoadState("networkidle");
const alertMessage = "Username or email is already taken";
@@ -51,6 +52,7 @@ test.describe("Signup Flow Test", async () => {
});
await page.goto("/signup");
await page.waitForLoadState("networkidle");
const alertMessage = "Username or email is already taken";
@@ -81,6 +83,7 @@ test.describe("Signup Flow Test", async () => {
// Signup with premium username name
await page.goto("/signup");
await page.waitForLoadState("networkidle");
// Fill form
await page.locator('input[name="username"]').fill("rock");
@@ -108,6 +111,7 @@ test.describe("Signup Flow Test", async () => {
});
await page.goto("/signup");
await page.waitForLoadState("networkidle");
// Fill form
await page.locator('input[name="username"]').fill(userToCreate.username);
@@ -115,10 +119,7 @@ test.describe("Signup Flow Test", async () => {
await page.locator('input[name="password"]').fill(userToCreate.password);
await page.click('button[type="submit"]');
await page.waitForLoadState("networkidle");
// Find the newly created user and add it to the fixture store
const newUser = await users.set(userToCreate.email);
expect(newUser).not.toBeNull();
await page.waitForURL("/auth/verify-email**");
// Check that the URL matches the expected URL
expect(page.url()).toContain("/auth/verify-email");
@@ -177,8 +178,8 @@ test.describe("Signup Flow Test", async () => {
});
const signupUrlWithToken = `/signup?token=${token}`;
await page.goto(signupUrlWithToken);
await page.waitForLoadState("networkidle");
const usernameField = page.locator('input[name="username"]');
const emailField = page.locator('input[name="email"]');
+1
View File
@@ -146,5 +146,6 @@ export const BOOKER_NUMBER_OF_DAYS_TO_LOAD = parseInt(
0
);
export const CLOUDFLARE_SITE_ID = process.env.NEXT_PUBLIC_CLOUDFLARE_SITEKEY;
export const MINIMUM_NUMBER_OF_ORG_SEATS = 30;
export const ORG_SELF_SERVE_ENABLED = process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED === "1";
@@ -0,0 +1,35 @@
import { HttpError } from "../http-error";
const TURNSTILE_SECRET_ID = process.env.CLOUDFLARE_TURNSTILE_SECRET;
export async function checkCfTurnstileToken({ token, remoteIp }: { token?: string; remoteIp: string }) {
// This means the instant doesnt have turnstile enabled - we skip the check and just return success.
// OR the instance is running in CI so we skip these checks also
if (!TURNSTILE_SECRET_ID || !!process.env.NEXT_PUBLIC_IS_E2E) {
return {
success: true,
};
}
if (!token) {
throw new HttpError({ statusCode: 401, message: "Invalid cloudflare token" });
}
const form = new URLSearchParams();
form.append("secret", TURNSTILE_SECRET_ID);
form.append("response", token);
form.append("remoteip", remoteIp);
const result = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
body: form,
});
const data = await result.json();
if (!data["success"]) {
throw new HttpError({ statusCode: 401, message: "Invalid cloudflare token" });
}
return data;
}
+2
View File
@@ -251,6 +251,7 @@
"CLOSECOM_API_KEY",
"CRON_API_KEY",
"CRON_ENABLE_APP_SYNC",
"CLOUDFLARE_TURNSTILE_SECRET",
"DAILY_API_KEY",
"DAILY_SCALE_PLAN",
"DAILY_WEBHOOK_SECRET",
@@ -321,6 +322,7 @@
"NEXT_PUBLIC_SWAGGER_DOCS_URL",
"NEXT_PUBLIC_TEAM_IMPERSONATION",
"NEXT_PUBLIC_VERCEL_URL",
"NEXT_PUBLIC_CLOUDFLARE_SITEKEY",
"NEXT_PUBLIC_VERCEL_ENV",
"NEXT_PUBLIC_VERCEL_BRANCH_URL",
"NEXT_PUBLIC_GTM_ID",
+138 -88
View File
@@ -290,25 +290,6 @@ __metadata:
languageName: node
linkType: hard
"@auth/core@npm:^0.1.4":
version: 0.1.4
resolution: "@auth/core@npm:0.1.4"
dependencies:
"@panva/hkdf": 1.0.2
cookie: 0.5.0
jose: 4.11.1
oauth4webapi: 2.0.5
preact: 10.11.3
preact-render-to-string: 5.2.3
peerDependencies:
nodemailer: 6.8.0
peerDependenciesMeta:
nodemailer:
optional: true
checksum: 64854404ea1883e0deb5535b34bed95cd43fc85094aeaf4f15a79e14045020eb944f844defe857edfc8528a0a024be89cbb2a3069dedef0e9217a74ca6c3eb79
languageName: node
linkType: hard
"@aw-web-design/x-default-browser@npm:1.4.126":
version: 1.4.126
resolution: "@aw-web-design/x-default-browser@npm:1.4.126"
@@ -4050,41 +4031,6 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/auth@workspace:apps/auth":
version: 0.0.0-use.local
resolution: "@calcom/auth@workspace:apps/auth"
dependencies:
"@auth/core": ^0.1.4
"@calcom/app-store": "*"
"@calcom/app-store-cli": "*"
"@calcom/config": "*"
"@calcom/core": "*"
"@calcom/dayjs": "*"
"@calcom/embed-core": "workspace:*"
"@calcom/embed-react": "workspace:*"
"@calcom/embed-snippet": "workspace:*"
"@calcom/features": "*"
"@calcom/lib": "*"
"@calcom/prisma": "*"
"@calcom/trpc": "*"
"@calcom/tsconfig": "*"
"@calcom/types": "*"
"@calcom/ui": "*"
"@types/node": 16.9.1
"@types/react": 18.0.26
"@types/react-dom": ^18.0.9
eslint: ^8.34.0
eslint-config-next: ^13.2.1
next: ^13.5.4
next-auth: ^4.22.1
postcss: ^8.4.18
react: ^18.2.0
react-dom: ^18.2.0
tailwindcss: ^3.3.3
typescript: ^4.9.4
languageName: unknown
linkType: soft
"@calcom/base@workspace:packages/platform/examples/base":
version: 0.0.0-use.local
resolution: "@calcom/base@workspace:packages/platform/examples/base"
@@ -4205,7 +4151,7 @@ __metadata:
chart.js: ^3.7.1
client-only: ^0.0.1
eslint: ^8.34.0
next: ^13.5.4
next: ^13.4.6
next-auth: ^4.22.1
next-i18next: ^13.2.2
postcss: ^8.4.18
@@ -5345,6 +5291,7 @@ __metadata:
react-schemaorg: ^2.0.0
react-select: ^5.7.0
react-timezone-select: ^1.4.0
react-turnstile: ^1.1.3
react-use-intercom: 1.5.1
remove-markdown: ^0.5.0
rrule: ^2.7.1
@@ -9424,6 +9371,13 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:13.5.6":
version: 13.5.6
resolution: "@next/env@npm:13.5.6"
checksum: 5e8f3f6f987a15dad3cd7b2bcac64a6382c2ec372d95d0ce6ab295eb59c9731222017eebf71ff3005932de2571f7543bce7e5c6a8c90030207fb819404138dc2
languageName: node
linkType: hard
"@next/env@npm:14.0.4":
version: 14.0.4
resolution: "@next/env@npm:14.0.4"
@@ -9463,6 +9417,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:13.5.6":
version: 13.5.6
resolution: "@next/swc-darwin-arm64@npm:13.5.6"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:14.0.4":
version: 14.0.4
resolution: "@next/swc-darwin-arm64@npm:14.0.4"
@@ -9484,6 +9445,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:13.5.6":
version: 13.5.6
resolution: "@next/swc-darwin-x64@npm:13.5.6"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:14.0.4":
version: 14.0.4
resolution: "@next/swc-darwin-x64@npm:14.0.4"
@@ -9505,6 +9473,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:13.5.6":
version: 13.5.6
resolution: "@next/swc-linux-arm64-gnu@npm:13.5.6"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:14.0.4":
version: 14.0.4
resolution: "@next/swc-linux-arm64-gnu@npm:14.0.4"
@@ -9526,6 +9501,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:13.5.6":
version: 13.5.6
resolution: "@next/swc-linux-arm64-musl@npm:13.5.6"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:14.0.4":
version: 14.0.4
resolution: "@next/swc-linux-arm64-musl@npm:14.0.4"
@@ -9547,6 +9529,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:13.5.6":
version: 13.5.6
resolution: "@next/swc-linux-x64-gnu@npm:13.5.6"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:14.0.4":
version: 14.0.4
resolution: "@next/swc-linux-x64-gnu@npm:14.0.4"
@@ -9568,6 +9557,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:13.5.6":
version: 13.5.6
resolution: "@next/swc-linux-x64-musl@npm:13.5.6"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:14.0.4":
version: 14.0.4
resolution: "@next/swc-linux-x64-musl@npm:14.0.4"
@@ -9589,6 +9585,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:13.5.6":
version: 13.5.6
resolution: "@next/swc-win32-arm64-msvc@npm:13.5.6"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:14.0.4":
version: 14.0.4
resolution: "@next/swc-win32-arm64-msvc@npm:14.0.4"
@@ -9610,6 +9613,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:13.5.6":
version: 13.5.6
resolution: "@next/swc-win32-ia32-msvc@npm:13.5.6"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:14.0.4":
version: 14.0.4
resolution: "@next/swc-win32-ia32-msvc@npm:14.0.4"
@@ -9631,6 +9641,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:13.5.6":
version: 13.5.6
resolution: "@next/swc-win32-x64-msvc@npm:13.5.6"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:14.0.4":
version: 14.0.4
resolution: "@next/swc-win32-x64-msvc@npm:14.0.4"
@@ -10273,13 +10290,6 @@ __metadata:
languageName: node
linkType: hard
"@panva/hkdf@npm:1.0.2":
version: 1.0.2
resolution: "@panva/hkdf@npm:1.0.2"
checksum: 75183b4d5ea816ef516dcea70985c610683579a9e2ac540c2d59b9a3ed27eedaff830a43a1c43c1683556a457c92ac66e09109ee995ab173090e4042c4c4bb03
languageName: node
linkType: hard
"@panva/hkdf@npm:^1.0.2":
version: 1.0.4
resolution: "@panva/hkdf@npm:1.0.4"
@@ -30855,13 +30865,6 @@ __metadata:
languageName: node
linkType: hard
"jose@npm:4.11.1":
version: 4.11.1
resolution: "jose@npm:4.11.1"
checksum: cd15cba258d0fd20f6168631ce2e94fda8442df80e43c1033c523915cecdf390a1cc8efe0eab0c2d65935ca973d791c668aea80724d2aa9c2879d4e70f3081d7
languageName: node
linkType: hard
"jose@npm:5.2.1":
version: 5.2.1
resolution: "jose@npm:5.2.1"
@@ -35179,6 +35182,61 @@ __metadata:
languageName: node
linkType: hard
"next@npm:^13.4.6":
version: 13.5.6
resolution: "next@npm:13.5.6"
dependencies:
"@next/env": 13.5.6
"@next/swc-darwin-arm64": 13.5.6
"@next/swc-darwin-x64": 13.5.6
"@next/swc-linux-arm64-gnu": 13.5.6
"@next/swc-linux-arm64-musl": 13.5.6
"@next/swc-linux-x64-gnu": 13.5.6
"@next/swc-linux-x64-musl": 13.5.6
"@next/swc-win32-arm64-msvc": 13.5.6
"@next/swc-win32-ia32-msvc": 13.5.6
"@next/swc-win32-x64-msvc": 13.5.6
"@swc/helpers": 0.5.2
busboy: 1.6.0
caniuse-lite: ^1.0.30001406
postcss: 8.4.31
styled-jsx: 5.1.1
watchpack: 2.4.0
peerDependencies:
"@opentelemetry/api": ^1.1.0
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
dependenciesMeta:
"@next/swc-darwin-arm64":
optional: true
"@next/swc-darwin-x64":
optional: true
"@next/swc-linux-arm64-gnu":
optional: true
"@next/swc-linux-arm64-musl":
optional: true
"@next/swc-linux-x64-gnu":
optional: true
"@next/swc-linux-x64-musl":
optional: true
"@next/swc-win32-arm64-msvc":
optional: true
"@next/swc-win32-ia32-msvc":
optional: true
"@next/swc-win32-x64-msvc":
optional: true
peerDependenciesMeta:
"@opentelemetry/api":
optional: true
sass:
optional: true
bin:
next: dist/bin/next
checksum: c869b0014ae921ada3bf22301985027ec320aebcd6aa9c16e8afbded68bb8def5874cca034c680e8c351a79578f1e514971d02777f6f0a5a1d7290f25970ac0d
languageName: node
linkType: hard
"next@npm:^13.5.4":
version: 13.5.5
resolution: "next@npm:13.5.5"
@@ -35833,13 +35891,6 @@ __metadata:
languageName: node
linkType: hard
"oauth4webapi@npm:2.0.5":
version: 2.0.5
resolution: "oauth4webapi@npm:2.0.5"
checksum: 32d0cb7b1cca42d51dfb88075ca2d69fe33172a807e8ea50e317d17cab3bc80588ab8ebcb7eb4600c371a70af4674595b4b341daf6f3a655f1efa1ab715bb6c9
languageName: node
linkType: hard
"oauth@npm:^0.9.15":
version: 0.9.15
resolution: "oauth@npm:0.9.15"
@@ -37970,17 +38021,6 @@ __metadata:
languageName: node
linkType: hard
"preact-render-to-string@npm:5.2.3":
version: 5.2.3
resolution: "preact-render-to-string@npm:5.2.3"
dependencies:
pretty-format: ^3.8.0
peerDependencies:
preact: ">=10"
checksum: 6e46288d8956adde35b9fe3a21aecd9dea29751b40f0f155dea62f3896f27cb8614d457b32f48d33909d2da81135afcca6c55077520feacd7d15164d1371fb44
languageName: node
linkType: hard
"preact-render-to-string@npm:^5.1.19":
version: 5.2.6
resolution: "preact-render-to-string@npm:5.2.6"
@@ -37992,7 +38032,7 @@ __metadata:
languageName: node
linkType: hard
"preact@npm:10.11.3, preact@npm:^10.6.3":
"preact@npm:^10.6.3":
version: 10.11.3
resolution: "preact@npm:10.11.3"
checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367
@@ -39772,6 +39812,16 @@ __metadata:
languageName: node
linkType: hard
"react-turnstile@npm:^1.1.3":
version: 1.1.3
resolution: "react-turnstile@npm:1.1.3"
peerDependencies:
react: ">= 16.13.1"
react-dom: ">= 16.13.1"
checksum: 1bb040eca9a4b24078dc21ee70947442ad97c7bc6507ba530974c4e5730769af0168e9b221e2835f20ea194282fc7a377eeb102084509f636fab322e95314201
languageName: node
linkType: hard
"react-twemoji@npm:^0.3.0":
version: 0.3.0
resolution: "react-twemoji@npm:0.3.0"