mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-18 11:32:15 -05:00
feat(captcha): add Vercel BotID provider (#11016)
Co-authored-by: Taesu <bytaesu@gmail.com> Co-authored-by: Taesu <166604494+bytaesu@users.noreply.github.com>
This commit is contained in:
co-authored by
Taesu
Taesu
parent
af883851ae
commit
3d0efa308d
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
Support Vercel BotID checks on protected authentication routes in Vercel-hosted applications.
|
||||
@@ -49,3 +49,4 @@ elif
|
||||
zitadel
|
||||
anthropics
|
||||
nextjs
|
||||
botid
|
||||
|
||||
@@ -10,14 +10,25 @@ The **Captcha Plugin** integrates bot protection into your Better Auth system by
|
||||
* [Cloudflare Turnstile](https://www.cloudflare.com/application-services/products/turnstile/)
|
||||
* [hCaptcha](https://www.hcaptcha.com/)
|
||||
* [CaptchaFox](https://captchafox.com/)
|
||||
* [Vercel BotID](https://vercel.com/docs/botid)
|
||||
|
||||
<Callout type="info">
|
||||
This plugin works out of the box with <Link href="/docs/authentication/email-password">Email & Password</Link> authentication. To use it with other authentication methods, you will need to configure the <Link href="/docs/plugins/captcha#plugin-options">endpoints</Link> array in the plugin options.
|
||||
The default endpoints cover <Link href="/docs/authentication/email-password">Email & Password</Link> authentication. Other authentication methods need an explicit <Link href="/docs/plugins/captcha#plugin-options">endpoints</Link> array. Vercel BotID also needs the client-side setup below.
|
||||
</Callout>
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install dependencies (Optional)
|
||||
|
||||
If you are using **Vercel BotID**, install its SDK in your application:
|
||||
|
||||
```package-install
|
||||
npm install botid
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add the plugin to your **auth** config
|
||||
|
||||
@@ -37,13 +48,13 @@ The **Captcha Plugin** integrates bot protection into your Better Auth system by
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add the captcha token to your request headers
|
||||
### Send the token for token-based providers
|
||||
|
||||
<Callout type="warning">
|
||||
The `x-captcha-user-remote-ip` header is no longer required—IP is now auto-detected server-side.
|
||||
</Callout>
|
||||
|
||||
Add the captcha token to your request headers for all protected endpoints. This example shows how to include it in a `signIn` request:
|
||||
For token-based providers, add the captcha token to requests for protected endpoints. This example shows a `signIn` request:
|
||||
|
||||
```ts
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
@@ -66,30 +77,89 @@ The **Captcha Plugin** integrates bot protection into your Better Auth system by
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Providers
|
||||
|
||||
### Vercel BotID
|
||||
|
||||
BotID requires a Vercel deployment and client-side setup. Follow [Vercel's setup guide](https://vercel.com/docs/botid/get-started) for the rewrites and client integration appropriate to your framework. The Better Auth plugin does not use a `secretKey` or an `x-captcha-response` header:
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { checkBotId } from "botid/server";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { captcha } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
captcha({
|
||||
provider: "vercel-botid",
|
||||
checkBotId,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
When using `initBotId`, protect Better Auth's default auth routes at their actual request paths:
|
||||
|
||||
```ts
|
||||
import { initBotId } from "botid/client/core";
|
||||
|
||||
initBotId({
|
||||
protect: [
|
||||
{ path: "/api/auth/sign-up/email", method: "POST" },
|
||||
{ path: "/api/auth/sign-in/email", method: "POST" },
|
||||
{ path: "/api/auth/request-password-reset", method: "POST" },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
BotID uses the browser's full request path. The plugin's `endpoints` option omits the Better Auth base path, as in `/sign-in/email`. Keep both in sync if you change `basePath` or `endpoints`.
|
||||
|
||||
With BotID initialized in the browser, sign in normally without a captcha token header:
|
||||
|
||||
```ts
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
const { data, error } = await authClient.signIn.email({
|
||||
email: "user@example.com",
|
||||
password: "secure-password",
|
||||
});
|
||||
```
|
||||
|
||||
BotID does not support [native HTML form submissions](https://vercel.com/docs/botid/form-submissions).
|
||||
|
||||
Local development [allows requests by default](https://vercel.com/docs/botid/local-development-behavior), so verify the full flow on Vercel before relying on it in production. For route-specific detection levels, follow [Vercel's advanced configuration](https://vercel.com/docs/botid/advanced-configuration).
|
||||
|
||||
## How it works
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
The plugin acts as a middleware: it intercepts all `POST` requests to configured endpoints (see `endpoints`
|
||||
The plugin acts as a middleware: it intercepts requests to configured endpoints (see `endpoints`
|
||||
in the [Plugin Options](#plugin-options) section).
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
it validates the captcha token on the server, by calling the captcha provider's `/siteverify`.
|
||||
|
||||
<Callout type="info">
|
||||
**Vercel BotID** is the exception: it uses the supplied `checkBotId` function instead of a captcha token or `/siteverify` call.
|
||||
</Callout>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
* if the token is missing, gets rejected by the captcha provider, or if the `/siteverify` endpoint is
|
||||
unavailable, the plugin returns an error and interrupts the request.
|
||||
* if the token is accepted by the captcha provider, the middleware returns `undefined`, meaning the request is allowed to proceed.
|
||||
* for **Vercel BotID**, the request is rejected when `checkBotId` (or your custom `validateRequest`) determines it's a bot, and allowed to proceed otherwise.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Plugin Options
|
||||
|
||||
* **`provider` (required)**: your captcha provider.
|
||||
* **`secretKey` (required)**: your provider's secret key used for the server-side validation.
|
||||
* **`secretKey` (required for all providers except `vercel-botid`)**: your provider's secret key used for the server-side validation.
|
||||
* `endpoints` (optional): replaces the default array of paths where captcha verification is enforced. If set, only the specified paths will be protected. Paths match exactly unless they include wildcards, such as `/sign-in/*` for one segment or `/sign-in/**` for nested routes. Default is `["/sign-up/email", "/sign-in/email", "/request-password-reset"]`.
|
||||
* `minScore` (optional - only *Google ReCAPTCHA v3*): minimum score threshold. Default is `0.5`.
|
||||
* `siteKey` (optional - only *hCaptcha* and *CaptchaFox*): prevents tokens issued on one sitekey from being redeemed elsewhere.
|
||||
* `siteVerifyURLOverride` (optional): overrides endpoint URL for the captcha verification request.
|
||||
* `siteVerifyURLOverride` (optional - token-based providers only): overrides the captcha verification URL.
|
||||
* `checkBotId` (required for *Vercel BotID*): Vercel's server-side check function, imported from `botid/server` in your application. Wrap it in a function if you need to pass SDK options.
|
||||
* `validateRequest` (optional - only *Vercel BotID*): custom validation logic. Return `true` to allow the request, `false` to reject it. By default, only results with `isBot: false` are allowed.
|
||||
|
||||
@@ -504,6 +504,7 @@
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "catalog:react19",
|
||||
"@types/react-dom": "catalog:react19",
|
||||
"botid": "^1.5.10",
|
||||
"happy-dom": "^20.8.9",
|
||||
"msw": "^2.14.6",
|
||||
"next": "^16.3.4",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as betterFetchModule from "@better-fetch/fetch";
|
||||
import { checkBotId as vercelCheckBotId } from "botid/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { getTestInstance } from "../../test-utils/test-instance";
|
||||
import { emailOTP } from "../email-otp";
|
||||
@@ -866,6 +867,198 @@ describe("captcha", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("vercel-botid", () => {
|
||||
it.each([
|
||||
"/sign-up/email",
|
||||
"/sign-in/email",
|
||||
"/request-password-reset",
|
||||
])("rejects bots on %s without a captcha token or secret key", async (path) => {
|
||||
const checkBotId = vi.fn(async () => ({ isBot: true }));
|
||||
const { auth } = await getTestInstance({
|
||||
plugins: [captcha({ provider: "vercel-botid", checkBotId })],
|
||||
});
|
||||
const res = await auth.handler(
|
||||
new Request(`http://localhost:3000/api/auth${path}`, {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(await res.json()).toMatchObject({
|
||||
code: "VERIFICATION_FAILED",
|
||||
});
|
||||
expect(checkBotId).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://vercel.com/docs/botid/local-development-behavior
|
||||
*/
|
||||
it("rejects the SDK's development BAD-BOT verdict", async () => {
|
||||
const checkBotId = () =>
|
||||
vercelCheckBotId({ developmentOptions: { bypass: "BAD-BOT" } });
|
||||
const { auth } = await getTestInstance({
|
||||
plugins: [captcha({ provider: "vercel-botid", checkBotId })],
|
||||
});
|
||||
const res = await auth.handler(
|
||||
new Request("http://localhost:3000/api/auth/sign-in/email", {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(await res.json()).toMatchObject({
|
||||
code: "VERIFICATION_FAILED",
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://vercel.com/docs/botid/local-development-behavior
|
||||
*/
|
||||
it("allows the SDK's development HUMAN verdict without a captcha token", async () => {
|
||||
const checkBotId = vi.fn(() =>
|
||||
vercelCheckBotId({ developmentOptions: { bypass: "HUMAN" } }),
|
||||
);
|
||||
const { client, testUser } = await getTestInstance({
|
||||
plugins: [captcha({ provider: "vercel-botid", checkBotId })],
|
||||
});
|
||||
const res = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
});
|
||||
|
||||
expect(res.error).toBeNull();
|
||||
expect(res.data?.user).toBeDefined();
|
||||
expect(checkBotId).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("allows a verified bot when custom validation opts in", async () => {
|
||||
const checkBotId = vi.fn(async () => ({
|
||||
isBot: true,
|
||||
isVerifiedBot: true,
|
||||
verifiedBotName: "trusted-agent",
|
||||
}));
|
||||
const { client, testUser } = await getTestInstance({
|
||||
plugins: [
|
||||
captcha({
|
||||
provider: "vercel-botid",
|
||||
checkBotId,
|
||||
validateRequest: ({ verification }) =>
|
||||
verification.isVerifiedBot === true &&
|
||||
verification.verifiedBotName === "trusted-agent",
|
||||
}),
|
||||
],
|
||||
});
|
||||
const res = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
});
|
||||
|
||||
expect(res.error).toBeNull();
|
||||
expect(res.data?.user).toBeDefined();
|
||||
});
|
||||
|
||||
it("rejects a human when custom validation rejects the request", async () => {
|
||||
const checkBotId = vi.fn(async () => ({ isBot: false }));
|
||||
const { client } = await getTestInstance({
|
||||
plugins: [
|
||||
captcha({
|
||||
provider: "vercel-botid",
|
||||
checkBotId,
|
||||
validateRequest: () => false,
|
||||
}),
|
||||
],
|
||||
});
|
||||
const res = await client.signIn.email({
|
||||
email: "test@test.com",
|
||||
password: "test123456",
|
||||
});
|
||||
|
||||
expect(res.error?.status).toBe(403);
|
||||
});
|
||||
|
||||
it("returns a generic 500 when the check fails", async () => {
|
||||
const checkBotId = vi.fn(async () => {
|
||||
throw new Error("private BotID failure");
|
||||
});
|
||||
const { client } = await getTestInstance({
|
||||
plugins: [captcha({ provider: "vercel-botid", checkBotId })],
|
||||
});
|
||||
const res = await client.signIn.email({
|
||||
email: "test@test.com",
|
||||
password: "test123456",
|
||||
});
|
||||
|
||||
expect(res.error?.status).toBe(500);
|
||||
expect(res.error?.code).toBe("UNKNOWN_ERROR");
|
||||
expect(res.error?.message).toBe("Something went wrong");
|
||||
});
|
||||
|
||||
it("does not check routes outside the protected endpoints", async () => {
|
||||
const checkBotId = vi.fn(async () => ({ isBot: true }));
|
||||
const { auth } = await getTestInstance({
|
||||
plugins: [captcha({ provider: "vercel-botid", checkBotId })],
|
||||
});
|
||||
const res = await auth.handler(
|
||||
new Request("http://localhost:3000/api/auth/ok"),
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(checkBotId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails closed when the check never resolves", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const checkBotId = () => new Promise<{ isBot: boolean }>(() => {});
|
||||
const { vercelBotId } = await import("./verify-handlers/vercel-botid");
|
||||
const resultPromise = vercelBotId({
|
||||
request: new Request("http://localhost/sign-in/email"),
|
||||
checkBotId,
|
||||
});
|
||||
const settled = vi.fn();
|
||||
void resultPromise.then(settled, settled);
|
||||
const assertion = expect(resultPromise).rejects.toThrow(
|
||||
"CAPTCHA service unavailable",
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(CAPTCHA_VERIFY_TIMEOUT_MS - 1);
|
||||
expect(settled).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
await assertion;
|
||||
expect(settled).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("fails closed when custom validation never resolves", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { vercelBotId } = await import("./verify-handlers/vercel-botid");
|
||||
const resultPromise = vercelBotId({
|
||||
request: new Request("http://localhost/sign-in/email"),
|
||||
checkBotId: async () => ({ isBot: false }),
|
||||
validateRequest: () => new Promise<boolean>(() => {}),
|
||||
});
|
||||
const settled = vi.fn();
|
||||
void resultPromise.then(settled, settled);
|
||||
const assertion = expect(resultPromise).rejects.toThrow(
|
||||
"CAPTCHA service unavailable",
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(CAPTCHA_VERIFY_TIMEOUT_MS - 1);
|
||||
expect(settled).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
await assertion;
|
||||
expect(settled).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("action and hostname binding", async () => {
|
||||
it("rejects a Turnstile token whose action does not match expectedAction", async () => {
|
||||
const log = vi.fn();
|
||||
|
||||
@@ -19,9 +19,13 @@ export const Providers = {
|
||||
GOOGLE_RECAPTCHA: "google-recaptcha",
|
||||
HCAPTCHA: "hcaptcha",
|
||||
CAPTCHAFOX: "captchafox",
|
||||
VERCEL_BOTID: "vercel-botid",
|
||||
} as const;
|
||||
|
||||
export const siteVerifyMap: Record<Provider, string> = {
|
||||
export const siteVerifyMap: Record<
|
||||
Exclude<Provider, typeof Providers.VERCEL_BOTID>,
|
||||
string
|
||||
> = {
|
||||
[Providers.CLOUDFLARE_TURNSTILE]:
|
||||
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||
[Providers.GOOGLE_RECAPTCHA]:
|
||||
|
||||
@@ -58,6 +58,14 @@ export const captcha = (options: CaptchaOptions) =>
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (options.provider === Providers.VERCEL_BOTID) {
|
||||
return await verifyHandlers.vercelBotId({
|
||||
request,
|
||||
checkBotId: options.checkBotId,
|
||||
validateRequest: options.validateRequest,
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.secretKey) {
|
||||
throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY.message);
|
||||
}
|
||||
|
||||
@@ -52,8 +52,55 @@ export interface CaptchaFoxOptions extends BaseCaptchaOptions {
|
||||
siteKey?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* BotID verdict fields available to custom request validation.
|
||||
*
|
||||
* @see https://vercel.com/docs/botid/verified-bots
|
||||
*/
|
||||
export type BotIdVerification = {
|
||||
isBot: boolean;
|
||||
isVerifiedBot?: boolean | undefined;
|
||||
verifiedBotName?: string | undefined;
|
||||
verifiedBotCategory?: string | undefined;
|
||||
};
|
||||
|
||||
export type ValidateRequestContext = {
|
||||
request: Request;
|
||||
verification: BotIdVerification;
|
||||
};
|
||||
|
||||
/**
|
||||
* Protect auth requests with Vercel BotID. The browser must also protect the
|
||||
* same request paths and methods with BotID's client SDK.
|
||||
*
|
||||
* @see https://vercel.com/docs/botid/get-started
|
||||
*/
|
||||
export interface VercelBotIdOptions {
|
||||
provider: typeof Providers.VERCEL_BOTID;
|
||||
/**
|
||||
* Auth paths to verify, without the Better Auth base path.
|
||||
*
|
||||
* @default ["/sign-up/email", "/sign-in/email", "/request-password-reset"]
|
||||
*/
|
||||
endpoints?: string[] | undefined;
|
||||
/**
|
||||
* Vercel's server-side check for the current request. Pass `checkBotId` from
|
||||
* `botid/server`, or wrap it to supply SDK options.
|
||||
*/
|
||||
checkBotId: () => Promise<BotIdVerification>;
|
||||
|
||||
/**
|
||||
* Override the default `isBot === false` decision. Return `true` to allow
|
||||
* the request, including a verified bot you trust.
|
||||
*
|
||||
* @see https://vercel.com/docs/botid/verified-bots
|
||||
*/
|
||||
validateRequest?: (ctx: ValidateRequestContext) => boolean | Promise<boolean>;
|
||||
}
|
||||
|
||||
export type CaptchaOptions =
|
||||
| GoogleRecaptchaOptions
|
||||
| CloudflareTurnstileOptions
|
||||
| HCaptchaOptions
|
||||
| CaptchaFoxOptions;
|
||||
| CaptchaFoxOptions
|
||||
| VercelBotIdOptions;
|
||||
|
||||
@@ -13,3 +13,21 @@ export const encodeToURLParams = (obj: Record<string, any>): string => {
|
||||
|
||||
return params.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Rejects after `timeoutMs` without cancelling the underlying promise.
|
||||
*/
|
||||
export const withTimeout = <T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
): Promise<T> => {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
const timeout = new Promise<never>((_resolve, reject) => {
|
||||
timer = setTimeout(
|
||||
() => reject(new Error("Operation timed out")),
|
||||
timeoutMs,
|
||||
);
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
||||
};
|
||||
|
||||
@@ -2,3 +2,4 @@ export { captchaFox } from "./captchafox";
|
||||
export { cloudflareTurnstile } from "./cloudflare-turnstile";
|
||||
export { googleRecaptcha } from "./google-recaptcha";
|
||||
export { hCaptcha } from "./h-captcha";
|
||||
export { vercelBotId } from "./vercel-botid";
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { middlewareResponse } from "../../../utils/middleware-response";
|
||||
import { CAPTCHA_VERIFY_TIMEOUT_MS } from "../constants";
|
||||
import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes";
|
||||
import type { BotIdVerification, ValidateRequestContext } from "../types";
|
||||
import { withTimeout } from "../utils";
|
||||
|
||||
type Params = {
|
||||
request: Request;
|
||||
checkBotId: () => Promise<BotIdVerification>;
|
||||
validateRequest?: (ctx: ValidateRequestContext) => boolean | Promise<boolean>;
|
||||
};
|
||||
|
||||
export const vercelBotId = async ({
|
||||
request,
|
||||
checkBotId,
|
||||
validateRequest,
|
||||
}: Params) => {
|
||||
let isValid: boolean;
|
||||
try {
|
||||
isValid = await withTimeout(
|
||||
(async () => {
|
||||
const verification = await checkBotId();
|
||||
return validateRequest
|
||||
? validateRequest({ request, verification })
|
||||
: verification.isBot === false;
|
||||
})(),
|
||||
CAPTCHA_VERIFY_TIMEOUT_MS,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE.message, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
if (isValid) return undefined;
|
||||
|
||||
return middlewareResponse({
|
||||
message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.message,
|
||||
code: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED.code,
|
||||
status: 403,
|
||||
});
|
||||
};
|
||||
Generated
+19
@@ -1109,6 +1109,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: catalog:react19
|
||||
version: 19.2.3(@types/react@19.2.17)
|
||||
botid:
|
||||
specifier: ^1.5.10
|
||||
version: 1.5.11(next@16.3.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(@types/node@25.9.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
|
||||
happy-dom:
|
||||
specifier: '>=20.8.9 <21'
|
||||
version: 20.8.9
|
||||
@@ -8386,6 +8389,17 @@ packages:
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
botid@1.5.11:
|
||||
resolution: {integrity: sha512-KOO1A3+/vFVJk5aFoG3sNwiogKFPVR+x4aw4gQ1b2e0XoE+i5xp48/EZn+WqR07jRHeDGwHWQUOtV5WVm7xiww==}
|
||||
peerDependencies:
|
||||
next: '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
next:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
boxen@8.0.1:
|
||||
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -24562,6 +24576,11 @@ snapshots:
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
botid@1.5.11(next@16.3.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(@types/node@25.9.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
|
||||
optionalDependencies:
|
||||
next: 16.3.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(@types/node@25.9.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
react: 19.2.7
|
||||
|
||||
boxen@8.0.1:
|
||||
dependencies:
|
||||
ansi-align: 3.0.1
|
||||
|
||||
Reference in New Issue
Block a user