diff --git a/content/guides/tune-a-protection-profile.mdx b/content/guides/tune-a-protection-profile.mdx index dd62c79..78ca11f 100644 --- a/content/guides/tune-a-protection-profile.mdx +++ b/content/guides/tune-a-protection-profile.mdx @@ -1,5 +1,8 @@ --- title: Tune a Protection Profile +description: + Learn how to tune your protection profiles to better protect your application while enhancing + your users' experiences. --- ### Throttling Windows diff --git a/content/index.mdx b/content/index.mdx index 60186d1..d4685f5 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -4,15 +4,87 @@ description: Welcome to Duckity's documentation site! icon: Fish --- -Welcome to Duckity's documentation site. Here you'll find everything you need to know about -Duckity, plus some extras to help you use Duckity more efficiently and effectively. +Welcome to Duckity's documentation site. Here you'll find everything you need to know about Duckity, +plus some extras to help you use Duckity more efficiently and effectively. -Duckity is an API-protection software. Unlike CAPTCHAs, which are black boxes and depend on AI -(known to make mistakes), Duckity relies on well-studied algorithms and even some things commonly -used to give you full control over access to your API. +## What Duckity Is -Duckity is NOT a replacement for basic security practices like rate limiting, and neither it is a -DDoS protection service. Use Cloudflare, Bunny.net, or similar for protection of that kind. +Duckity simplifies your API's protection, makes it predictable, and increases the cost an attacker +has to pay to abuse your system. + + + }> + Duckity adds an invisible-to-the-user non-parallelizable CPU cost on every protected request + that increases the cost of abuse. + + }> + Keep full control over your protection's behavior by tuning parameters from the dashboard. + Nothing is implicit. + + }> + Monitor the system's state at any time. Access patterns are logged for as long as you + configure them to audit behavior in real time. + + }> + Duckity provides cross-platform SDKs for you to use the same protection system everywhere. + Get started in less than 5 minutes. + + + +By layering dynamic CPU cost on top of configurable access rules and real-time adaptive costs, users +stop being annoyed by intrusive CAPTCHA puzzles while abusers get an increasing cost. + +Optimized for scale and heavy concurrent access, Duckity automatically scales with your system +without hassle. + +## Benefits Over CAPTCHAs and Alternatives + +Duckity provides several improvements over standard CAPTCHAs and CAPTCHA alternatives: + + + + By relying on modern verifiable delay functions, Duckity is able to provide hands-off + security to your system and customers without sacrificing on user experience. Abuse cost + scales dynamically while your users get a predictable application experience, unlike + alternatives where delays vary substantially depending on randomness. + + + Duckity provides multiple easy-to-use cross-platform SDKs for you to use a single + configuration across all your clients. From web to native desktop and mobile applications, + CLI applications, M2M connections, videogames, IoT, and more. + + + Configure Duckity's behavior to suit your application's precise needs. Multiple features can + get individual configurations to beter suit each of their access patterns and requirements. + + + Most SDKs require no more than 4 lines of code to integrate with your application. Duckity + provides detailed documentation for every feature to help you make the most out of it. + + + +Duckity does not try to tell humans and bots apart. Instead, it enforces a background computational +cost that humans do not notice. + +| | Duckity | CAPTCHAs | +| --------------------------- | ------- | -------- | +| Simple | ✅ | ✅ | +| Transparent | ✅ | ❌ | +| Predictable behavior | ✅ | ❌ | +| Customizable behavior | ✅ | ❌ | +| Frictionless | ✅ | ❌ | +| Cross-platform | ✅ | ❌ | +| Accessible | ✅ | ❌ | +| Tells humans and bots apart | ❌ | ❓ | +| Cost-effective | ✅ | ❓ | +| AI-resistant | ✅ | ❓ | + +## Get Started + +You can make a free account by heading over to the [Duckity Dashboard](//app.duckity.com/hello). To +learn how to integrate with your stack in some minutes, head over to the Quick Start page. + +}> + Integrate Duckity in less than 5 minutes. + -If you're new to Duckity, head over to the [Quick Start](/quick-start). It'll guide you on how to get started and -give you some templates to reduce the boilerplate configuration tasks. diff --git a/content/meta.json b/content/meta.json index ed563c9..bd291bb 100644 --- a/content/meta.json +++ b/content/meta.json @@ -4,12 +4,8 @@ "index", "quick-start", "---[BookOpenText]Guides---", - "guides/tune-a-protection-profile", - "guides/write-your-own-sdk", + "...guides", "---[CodeXml]Integrate---", - "sdks/javascript", - "sdks/react", - "sdks/python", - "sdks/rust" + "...sdks" ] } \ No newline at end of file diff --git a/content/quick-start.mdx b/content/quick-start.mdx index 194f26e..b2e1cd0 100644 --- a/content/quick-start.mdx +++ b/content/quick-start.mdx @@ -1,6 +1,6 @@ --- title: Quick Start -description: Learn how to use Duckity in some minutes. +description: Integrate Duckity in less than 5 minutes. icon: Zap --- @@ -44,36 +44,575 @@ to integrate it into your application. Don't worry about all the settings displayed for now, you'll learn to tune them later. -## Install an SDK [step] +## Install a Client SDK [step] -To integrate your protection profile with your application, you'll need a client-side SDK and a -server-side SDK. +To integrate your protection profile into your application, you'll need to install a client SDK in +your client and validate solution tokens from your server. This is very simple to do and will take +you little to no time. -Duckity has multiple official SDKs for you to plug and play. + + + + + Run the following line in your terminal to install the Duckity SDK. -import { SiC, SiJavascript, SiPython, SiRust, SiReact } from "@icons-pack/react-simple-icons"; + ```package-install + @duckity/js + ``` - - }> - Integrate Duckity into your application using the JavaScript SDK. - - }> - Integrate Duckity into your React app using the React SDK. - - }> - Integrate Duckity into your application using the Python SDK. - - }> - Integrate Duckity into your application using the Rust SDK. Supports no-std environments. - - + Then import it in your code: -In case we don't have a client SDK for your language yet, you can use the C SDK. It's written in -Rust, you can link to it through the exposed C API, and it wraps all the required encoding and -decoding in a simple API. You may also find the following guide useful: + ```ts + import * as duckity from "@duckity/js"; + ``` + + + If you're using the SDK from a static site, import it using a CDN like + [esm.sh](https://esm.sh/) instead. -import { FileCodeCorner } from "lucide-react"; + ```html + + ``` + + + + + Once you've imported the SDK, solve a challenge like follows: + + ```ts + async function handleSubmit(e) { + e.preventDefault(); + const solution: string = await duckity.solve(PROTECTION_PROFILE_ID); + // Send it to your backend. + } + ``` + + Send that solution token to your backend server. It's usually a good idea to send it in a + `X-Duckity-Solution` header in your request, for example + + ```ts + await fetch("/api/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "X-Duckity-Solution": solution + }, + body: JSON.stringify({ + login: username, + password: password, + }), + }); + ``` + + You can read about advanced usage in the SDK's documentation page: + + }> + Integrate Duckity into your application using the JavaScript SDK. + + + + Run the following line in your terminal to install the Duckity SDK. + + ```package-install + @duckity/react + ``` + + Then import the React hook in your code: + + ```ts + import { useChallenge } from "@duckity/react"; + ``` + + You can then use the hook in your client components: + + + + ```ts + "use client"; + + import { useChallenge } from "@duckity/react"; + + function MyComponent() { + const duckity = useChallenge(process.env.NEXT_PUBLIC_DUCKITY_PROTECTION_PROFILE_ID); + } + ``` + + + ```ts + import { useChallenge } from "@duckity/react"; + + function MyComponent() { + const duckity = useChallenge(import.meta.env.VITE_DUCKITY_PROTECTION_PROFILE_ID); + } + ``` + + + ```ts + import { json } from "@remix-run/node"; // Or cloudflare/deno + import { useLoaderData } from "@remix-run/react"; + import { useChallenge } from "@duckity/react"; + + export async function loader() { + return json({ + ENV: { + DUCKITY_PROTECTION_PROFILE_ID: process.env.DUCKITY_PROTECTION_PROFILE_ID, + }, + }); + } + + function MyComponent() { + const data = useLoaderData(); + const duckity = useChallenge(data.ENV.DUCKITY_PROTECTION_PROFILE_ID); + } + ``` + + + ```ts + import { useChallenge } from "@duckity/react"; + + function MyComponent() { + const duckity = useChallenge(process.env.GATSBY_DUCKITY_PROTECTION_PROFILE_ID); + } + ``` + + + ```ts + import { useChallenge } from "@duckity/react"; + + function MyComponent() { + const duckity = useChallenge(process.env.EXPO_PUBLIC_DUCKITY_PROTECTION_PROFILE_ID); + } + ``` + + + ```ts + import { useChallenge } from "@duckity/react"; + + function MyComponent() { + const duckity = useChallenge(import.meta.env.PUBLIC_DUCKITY_PROTECTION_PROFILE_ID); + } + ``` + + This is a client component, so use it as follows when rendering it: + + ```tsx + + ``` + + + + + When the component is first rendered, it'll start fetching and solving a challenge. + + You can then wait for and use a solution token: + + ```ts + async function handleSubmit(e) { + e.preventDefault(); + const solution = await duckity.wait(); + // Send the solution token to your backend. + } + ``` + + Send that solution token to your backend server. It's usually a good idea to send it in a + `X-Duckity-Solution` header in your request, for example + + ```ts + await fetch("/api/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "X-Duckity-Solution": solution + }, + body: JSON.stringify({ + login: username, + password: password, + }), + }); + ``` + + You can read about advanced usage in the SDK's documentation page: + + }> + Integrate Duckity into your application using the React SDK. + + + + Install the SDK from PyPI using your favorite package manager: + + {/* prettier-ignore */} + + + ```sh + pip install duckity + ``` + + + ```sh + uv add duckity + ``` + + + ```sh + poetry add duckity + ``` + + + + Solving a challenge only requires your protection profile ID. + + + + ```py + import duckity + + solution: str = await duckity.solve(PROTECTION_PROFILE_ID) + ``` + + The CPU-intensive part of solving the challenge is done in a + `ProcessPoolExecutor()`. Neither the GIL nor the async event loop will be blocked. + + + ```py + import asyncio + + import duckity + + solution: str = asyncio.run(duckity.solve(PROTECTION_PROFILE_ID)) + ``` + + The CPU-intensive part of solving the challenge is done in a + `ProcessPoolExecutor()`. The GIL will not be blocked. + + + + Once you have a solution token, send it to your backend for validation. It's usually a good + idea to send it in a `X-Duckity-Solution` request header, like follows: + + ```py + import requests + + solution: str = asyncio.run(duckity.solve(PROTECTION_PROFILE_ID)) + + requests.post( + "https://api.example.com/login", + headers={ + "Accept": "application/json", + "X-Duckity-Solution": solution, + }, + json={ + "login": username, + "password": password + } + ) + ``` + + You can read about advanced usage in the SDK's documentation page: + + }> + Integrate Duckity into your application using the Python SDK. + + + + Run the following cargo command in your terminal to add the `duckity` crate to your + project: + + ```sh + cargo add duckity + ``` + + The crate depends on `tokio`, you can use `async-compat` to make it work in `smol` + contexts. + + To get and solve a challenge, use `duckity::solve()` like follows: + + ```rs + let solution: str = duckity::solve(PROTECTION_PROFILE_ID).await?; + ``` + + That's it! Once you have your solution token, you can send it over to your backend for + validation. It's usually a good idea to send it in a `X-Duckity-Solution` request header, + like follows: + + ```rs + reqwest::Client::new() + .post("https://api.example.com/login") + .header("x-duckity-solution", solution) + .json(&Login { + login: username, + password: password, + }) + .send() + .await?; + ``` + + You can read about advanced usage in the SDK's documentation page: + + }> + Integrate Duckity into your application using the Rust SDK. + + + + In case your platform is a different one, we provide a detailed step-by-step guide on how + to write your own client-side SDK implementation in any language, with code examples and + pseudocode. It's written so that anyone can implement a working client-side implementation + for any platform, so if you have a bit of time, you may be able to get it working in less + than an hour. + + }> + Integrate Duckity into your application in any programming language and platform. + + + We constantly improve this guide with incoming feedback from developers like you. If you + find anything that could be improved, let us know! + + Additionally, in case anything in the guide is not clear, open a support ticket in the + dashboard and we'll try to help you out. Note that we're not experts in every language nor + platform, so we likely won't be able to debug your code, rather help you understand what + the guide says. + + + + +## Install a Server SDK [step] + +Once you have a solution token in your server, validating it is quite easy. + + + + Run the following line in your terminal to install the Duckity SDK. + + ```package-install + @duckity/js + ``` + + Then import it in your code: + + ```ts + import * as duckity from "@duckity/js"; + ``` + + To validate the solution token, use `duckity.validate()`. + + ```ts + let isValid = await duckity.validate( + solution, // The solution token submitted by the client. + clientIp, // The IP address of the client that submitted the solution. + applicationSecret, // Your application's secret. Get it from the dashboard. + protectionProfileId, // The protection profile ID used to generate the challenge. + ); + ``` + + That's it! If the solution is not valid, reject the request with an error. Otherwise, + proceed to process the rest of the request. + + + Install the SDK from PyPI using your favorite package manager: + + {/* prettier-ignore */} + + + ```sh + pip install duckity + ``` + + + ```sh + uv add duckity + ``` + + + ```sh + poetry add duckity + ``` + + + + Once you have done it, validate the solution token as follows: + + + + ```py + import duckity + + solution: str # The solution token submitted by the client. + client_ip: str # The IP address of the client that submitted the solution. + application_secret: str # Your application's secret. Get it from the dashboard. + protection_profile_id: str # The protection profile ID used to generate the challenge. + + is_valid: bool = await duckity.validate( + solution, client_ip, application_secret, protection_profile_id + ) + ``` + + + ```py + import asyncio + + import duckity + + solution: str # The solution token submitted by the client. + client_ip: str # The IP address of the client that submitted the solution. + application_secret: str # Your application's secret. Get it from the dashboard. + protection_profile_id: str # The protection profile ID used to generate the challenge. + + is_valid: bool = asyncio.run( + duckity.validate(solution, client_ip, application_secret, protection_profile_id) + ) + ``` + + + + + That's it! If the solution token is valid, you can proceed to process your request. If it's + not, return an error to the client and do not further process the request. + + + Run the following cargo command in your terminal to add the `duckity` crate to your + project: + + ```sh + cargo add duckity + ``` + + Once you have it installed, you can validate solution tokens as follows: + + ```rs + let is_valid: bool = duckity::validate( + solution, // The solution token submitted by the client. + client_ip, // The IP address of the client that submitted the solution. + APPLICATION_SECRET, // Your application's secret. Get it from the dashboard. + PROTECTION_PROFILE_ID // The protection profile ID used to generate the challenge. + ) + .await?; + ``` + + That's it! If the solution token is valid, you can proceed to process your request. If it's + not, return an error to the client and do not further process the request. + + + In case you cannot or do not want to install an SDK, you can validate solution tokens by + hand by making the HTTP request by hand instead. + + ```http + POST /d1/challenges/{protection_profile_id}/validate HTTP/1.1 + Host: api.duckity.com + Accept: application/json + Content-Type: application/json + Authorization: Bearer {application_secret} + + { + "solution": "", + "ip": "" + } + ``` + + Validation responses are simple: + + + + ```http + HTTP/1.1 200 OK + Content-Type: application/json + + { + "is_valid": true // or false + } + ``` + + + ```http + HTTP/1.1 401 Unauthorized + Content-Type: application/json + + { + "title": "Invalid Application Secret", + "message": "The application secret you provided was not valid." + } + ``` + + + ```http + HTTP/1.1 404 Not Found + Content-Type: application/json + + { + "title": "Protection Profile Not Found", + "message": "The protection profile ID you specified was not found." + } + ``` + + + ```http + HTTP/1.1 422 Unprocessable Entity + Content-Type: application/json + + { + "title": "Invalid Request Body", + "message": "Are you sure the JSON body you sent in the request was valid?" + } + ``` + + + ```http + HTTP/1.1 429 Too Many Requests + Content-Type: application/json + + { + "title": "Too Many Requests", + "message": "Wohoah, you're going too fast, buddy. Slow down a bit." + } + ``` + + + ```http + HTTP/1.1 500 Internal Server Error + Content-Type: application/json + + { + "title": "Internal Server Error", + "message": "The server crashed. Please try again." + } + ``` + + + + That's it! If the solution token is valid, you can proceed to process your request. If it's + not, return an error to the client and do not further process the request. + + Note that status codes are related to server actions, not to client actions. An invalid + solution token submitted by the client will return `200` because the token was validated + successfully (even if the result was that it was invalid), while a wrong application secret + or an incorrect protection profile ID will return `4xx` status codes. + + + + +## Next Steps + +Once you have successfully integrated duckity into your application, you're done with the code part. +The behavior of challenges issued is fully customizable from the Duckity dashboard. Almost every +configuration can be easily understood by their dashboard description. However, to learn how to +combine settings to produce a more powerful setup, check out the following guide. It goes +configuration by configuration to teach you how to configure the parameters you need to suit your +application's needs. + +} +> + Learn how to tune your protection profiles to better protect your application while enhancing + your users' experiences. diff --git a/next.config.mjs b/next.config.mjs index 0682f30..ef0f632 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -13,6 +13,16 @@ const config = { images: { unoptimized: true, }, + async rewrites() { + return { + beforeFiles: [ + { + source: "/:path*.md", + destination: "/llms.mdx/docs/:path*", + }, + ], + }; + }, }; export default withMDX(config); diff --git a/src/components/accordion.tsx b/src/components/accordion.tsx new file mode 100644 index 0000000..e9d172b --- /dev/null +++ b/src/components/accordion.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { Check, LinkIcon } from 'lucide-react'; +import { type ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react'; +import { cn } from '../lib/cn'; +import { useCopyButton } from '@fumadocs/base-ui/utils/use-copy-button'; +import { buttonVariants } from './ui/button'; +import { mergeRefs } from '../lib/merge-refs'; +import { useTranslations } from '@fuma-translate/react'; +import { + Accordion as Root, + AccordionContent, + AccordionHeader, + AccordionItem, + AccordionTrigger, +} from './ui/accordion'; + +export function Accordions({ + ref, + className, + defaultValue, + ...props +}: ComponentProps) { + const rootRef = useRef(null); + const composedRef = mergeRefs(ref, rootRef); + const [value, setValue] = useState(defaultValue ?? []); + + useEffect(() => { + const id = window.location.hash.substring(1); + const element = rootRef.current; + if (!element || id.length === 0) return; + + const selected = document.getElementById(id); + if (!selected || !element.contains(selected)) return; + const value = selected.getAttribute('data-accordion-value'); + + if (value) setValue((prev) => [value, ...prev]); + }, []); + + return ( + + cn( + 'divide-y divide-fd-border overflow-hidden rounded-lg border bg-fd-card', + typeof className === 'function' ? className(s) : className, + ) + } + {...props} + /> + ); +} + +export function Accordion({ + title, + id, + value = String(title), + children, + ...props +}: Omit, 'value' | 'title'> & { + title: string | ReactNode; + value?: string; +}) { + return ( + + + {title} + {id ? : null} + + +
+ {children} +
+
+
+ ); +} + +function CopyButton({ id }: { id: string }) { + const t = useTranslations({ note: 'accordion' }); + const [checked, onClick] = useCopyButton(() => { + const url = new URL(window.location.href); + url.hash = id; + + return navigator.clipboard.writeText(url.toString()); + }); + + return ( + + ); +} diff --git a/src/components/mdx.tsx b/src/components/mdx.tsx index e0d54c4..3f3c67d 100644 --- a/src/components/mdx.tsx +++ b/src/components/mdx.tsx @@ -1,12 +1,31 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; -import * as TabsComponents from 'fumadocs-ui/components/tabs'; +import * as TabsComponents from "fumadocs-ui/components/tabs"; import Image from "next/image"; -import { Mermaid } from '@/components/mermaid'; +import { Mermaid } from "@/components/mermaid"; import type { MDXComponents } from "mdx/types"; -import * as Twoslash from 'fumadocs-twoslash/ui'; -import { TypeTable } from 'fumadocs-ui/components/type-table'; +import * as Twoslash from "fumadocs-twoslash/ui"; +import { TypeTable } from "fumadocs-ui/components/type-table"; +import * as reactSimpleIcons from "@icons-pack/react-simple-icons"; +import { icons as reactLucideIcons } from "lucide-react"; +import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; export function getMDXComponents(components?: MDXComponents) { + let simpleIcons: { [key: string]: any } = {}; + for (const key in reactSimpleIcons) { + if (!key.endsWith("Hex")) { + let newKey = "Si" + key.substring(2); + simpleIcons[newKey] = (reactSimpleIcons as { [key: string]: any })[key]; + } + } + + let lucideIcons: { [key: string]: any } = {}; + for (const key in reactLucideIcons) { + if (!key.endsWith("Icon")) { + let newKey = "Li" + key; + lucideIcons[newKey] = (reactLucideIcons as { [key: string]: any })[key].render; + } + } + return { ...defaultMdxComponents, img: (props) => , @@ -14,6 +33,10 @@ export function getMDXComponents(components?: MDXComponents) { ...Twoslash, Mermaid, TypeTable, + ...simpleIcons, + ...lucideIcons, + Accordion, + Accordions, ...components, } satisfies MDXComponents; } diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..d6d3865 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { Accordion as Primitive } from '@base-ui/react/accordion'; +import { ChevronRight } from 'lucide-react'; +import { type ComponentProps } from 'react'; +import { cn } from '../../lib/cn'; + +export function Accordion({ className, ...props }: ComponentProps) { + return ( + + cn( + 'divide-y divide-fd-border overflow-hidden rounded-lg border bg-fd-card', + typeof className === 'function' ? className(s) : className, + ) + } + {...props} + /> + ); +} + +export function AccordionItem({ children, ...props }: ComponentProps) { + return {children}; +} + +export function AccordionHeader({ + className, + children, + ...props +}: ComponentProps) { + return ( + + cn( + 'scroll-m-24 not-prose flex flex-row items-center text-fd-card-foreground font-medium has-focus-visible:bg-fd-accent', + typeof className === 'function' ? className(s) : className, + ) + } + {...props} + > + {children} + + ); +} + +export function AccordionTrigger({ + className, + children, + ...props +}: ComponentProps) { + return ( + + cn( + 'group flex flex-1 items-center gap-2 px-3 py-2.5 text-start focus-visible:outline-none', + typeof className === 'function' ? className(s) : className, + ) + } + {...props} + > + + {children} + + ); +} + +export function AccordionContent({ + className, + children, + ...props +}: ComponentProps) { + return ( + + cn( + 'h-(--accordion-panel-height) overflow-hidden transition-[height] ease-out data-ending-style:h-0 data-starting-style:h-0', + typeof className === 'function' ? className(s) : className, + ) + } + {...props} + > + {children} + + ); +}