Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement bot detection categories #1618

Merged
merged 7 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core.wasm
Binary file not shown.
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.wasm
Binary file not shown.
5 changes: 3 additions & 2 deletions arcjet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ArcjetSensitiveInfoRule,
ArcjetIdentifiedEntity,
ArcjetWellKnownBot,
ArcjetBotCategory,
} from "@arcjet/protocol";
import {
ArcjetBotReason,
Expand Down Expand Up @@ -269,14 +270,14 @@ type SlidingWindowRateLimitOptions<Characteristics extends readonly string[]> =

type BotOptionsAllow = {
mode?: ArcjetMode;
allow: Array<ArcjetWellKnownBot>;
allow: Array<ArcjetWellKnownBot | ArcjetBotCategory>;
deny?: never;
};

type BotOptionsDeny = {
mode?: ArcjetMode;
allow?: never;
deny: Array<ArcjetWellKnownBot>;
deny: Array<ArcjetWellKnownBot | ArcjetBotCategory>;
};

export type BotOptions = BotOptionsAllow | BotOptionsDeny;
Expand Down
2 changes: 2 additions & 0 deletions examples/nextjs-14-bot-categories/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Add your Arcjet key from https://app.arcjet.com
ARCJET_KEY=
3 changes: 3 additions & 0 deletions examples/nextjs-14-bot-categories/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
36 changes: 36 additions & 0 deletions examples/nextjs-14-bot-categories/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
1 change: 1 addition & 0 deletions examples/nextjs-14-bot-categories/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
omit=optional
66 changes: 66 additions & 0 deletions examples/nextjs-14-bot-categories/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<a href="https://arcjet.com" target="_arcjet-home">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://arcjet.com/logo/arcjet-dark-lockup-voyage-horizontal.svg">
<img src="https://arcjet.com/logo/arcjet-light-lockup-voyage-horizontal.svg" alt="Arcjet Logo" height="128" width="auto">
</picture>
</a>

# Arcjet Bot Detection leveraging categories with Next.js 14

This example shows how to use Arcjet to allow detected bots based on categories,
individual selection, and filtering bots out of defined categories.

## How to use

1. From the root of the project, install the SDK dependencies.

```bash
npm ci
```

2. Enter this directory and install the example's dependencies.

```bash
cd examples/nextjs-14-bot-categories
npm ci
```

3. Rename `.env.local.example` to `.env.local` and add your Arcjet key.

4. Start the dev server.

```bash
npm run dev
```

5. Use curl to access the API and display headers:
```sh
curl -v localhost:3000/api/arcjet
```
These headers in our terminal output inform us that `CURL` was detected and
allowed because of `CATEGORY:TOOL`.
```txt
x-arcjet-bot-allowed: CATEGORY:TOOL, CURL
x-arcjet-bot-denied:
```
6. Now, change the User-Agent we are sending:
```sh
curl -v -A "vercel-screenshot" localhost:3000/api/arcjet
```
These headers inform us that `VERCEL_MONITOR_PREVIEW` was detected and
allowed, but it did not belong to our selected categories.
```txt
x-arcjet-bot-allowed: VERCEL_MONITOR_PREVIEW
x-arcjet-bot-denied:
```
7. Finally, pretend to be Google's AdsBot:
```sh
curl -v -A "AdsBot-Google" localhost:3000/api/arcjet
```
These headers inform us that `GOOGLE_ADSBOT` was detected and blocked. It
does not list the `CATEGORY:GOOGLE` because we programatically filtered the
list, which translates our category into all individual items.
```txt
x-arcjet-bot-allowed:
x-arcjet-bot-denied: GOOGLE_ADSBOT
```
48 changes: 48 additions & 0 deletions examples/nextjs-14-bot-categories/app/api/arcjet/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import arcjet, { botCategories, detectBot } from "@arcjet/next";
import { NextResponse } from "next/server";

const aj = arcjet({
// Get your site key from https://app.arcjet.com and set it as an environment
// variable rather than hard coding.
key: process.env.ARCJET_KEY,
rules: [
// Detect bots with the ability to allow or deny subsets
detectBot({
mode: "LIVE",
// explicitly allow bots in the list while denying all others
allow: [
// allow any developer tools, such as the curl command
"CATEGORY:TOOL",
// allow a single detected bot, such as Vercel's screenshot bot
"VERCEL_MONITOR_PREVIEW",
// filter a category to remove individual bots from our provided lists
...botCategories["CATEGORY:GOOGLE"].filter((bot) => bot !== "GOOGLE_ADSBOT" && bot !== "GOOGLE_ADSBOT_MOBILE")
]
// deny: [] // explicitly deny bots in the list while allowing all others
}),
],
});

export async function GET(req: Request) {
const decision = await aj.protect(req);

const headers = new Headers();
if (decision.reason.isBot()) {
// WARNING: This is illustrative! Don't share this metadata with users;
// otherwise they may use it to subvert bot detection!
headers.set("X-Arcjet-Bot-Allowed", decision.reason.allowed.join(", "))
headers.set("X-Arcjet-Bot-Denied", decision.reason.denied.join(", "))
}

if (decision.isDenied()) {
return NextResponse.json(
{ error: "You are a Bot!" },
{ status: 403, headers },
);
}

return NextResponse.json(
{ message: "Hello World" },
{ status: 200, headers }
);
}
Binary file not shown.
27 changes: 27 additions & 0 deletions examples/nextjs-14-bot-categories/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
22 changes: 22 additions & 0 deletions examples/nextjs-14-bot-categories/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
113 changes: 113 additions & 0 deletions examples/nextjs-14-bot-categories/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import Image from 'next/image'

export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>

<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>

<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Docs{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Find in-depth information about Next.js features and API.
</p>
</a>

<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Learn{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>

<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Templates{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Explore starter templates for Next.js.
</p>
</a>

<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Deploy{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
)
}
6 changes: 6 additions & 0 deletions examples/nextjs-14-bot-categories/cache/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"telemetry": {
"notifiedAt": "1709580154634",
"enabled": false
}
}
5 changes: 5 additions & 0 deletions examples/nextjs-14-bot-categories/environment.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare namespace NodeJS {
export interface ProcessEnv {
readonly ARCJET_KEY: string;
}
}
4 changes: 4 additions & 0 deletions examples/nextjs-14-bot-categories/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = nextConfig
Loading