diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..882236c
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,26 @@
+{
+ "overrides": [
+ {
+ "files": [
+ "*.tsx",
+ "*.jsx",
+ "*.ts",
+ "*.js"
+ ],
+ "options": {
+ "tabWidth": 2
+ }
+ },
+ {
+ "files": [
+ "*.mdx",
+ "*.md"
+ ],
+ "options": {
+ "tabWidth": 4,
+ "proseWrap": "always",
+ "printWidth": 100
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/bun.lock b/bun.lock
index 20b3cfc..c68fcb2 100644
--- a/bun.lock
+++ b/bun.lock
@@ -14,6 +14,7 @@
"@typescript/vfs": "^1.6.4",
"beautiful-mermaid": "^1.1.3",
"class-variance-authority": "^0.7.1",
+ "cn": "^0.2.4",
"cnfast": "^0.0.8",
"fumadocs-core": "16.12.1",
"fumadocs-mdx": "15.2.0",
@@ -415,6 +416,8 @@
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+ "cn": ["cn@0.2.5", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-OCjZtMeQfXbI4Es1+EIjkd77gvWzaE689gD8KhfexlqjClC06qR1MQBR+Z35ZMSPNEBWyHiItW1Soy0UvwNv9w=="],
+
"cnfast": ["cnfast@0.0.8", "", { "bin": { "cnfast": "bin/cli.js" } }, "sha512-EjXKMfGfdwtV4AcNSQ6AwQaVzpC1B7IxeiwA3FlhTXz+YFlMKVi4c1JX9tgD2QOlahQXjB8KUXrBaYG+3v871Q=="],
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
diff --git a/content/guides/write-your-own-sdk.mdx b/content/guides/write-your-own-sdk.mdx
index 8a62974..8b26e95 100644
--- a/content/guides/write-your-own-sdk.mdx
+++ b/content/guides/write-your-own-sdk.mdx
@@ -711,7 +711,7 @@ In TypeScript, this equals to
-#### `isPrimeForBase` [step]
+#### `is_prime_for_base` [step]
This function will test a number using Miller-Rabin for the specified base. By later testing with
different bases in `is_prime()`, we increase the likelyhood of a number being prime. The test works
diff --git a/content/meta.json b/content/meta.json
index 25bf45d..ed563c9 100644
--- a/content/meta.json
+++ b/content/meta.json
@@ -9,10 +9,7 @@
"---[CodeXml]Integrate---",
"sdks/javascript",
"sdks/react",
- "sdks/c",
"sdks/python",
- "sdks/rust-std",
- "sdks/rust-no-std",
- "sdks/go"
+ "sdks/rust"
]
}
\ No newline at end of file
diff --git a/content/quick-start.mdx b/content/quick-start.mdx
index 8c6d812..194f26e 100644
--- a/content/quick-start.mdx
+++ b/content/quick-start.mdx
@@ -10,19 +10,18 @@ first time logging in, your account will be automatically created.
## Create an Application [step]
-Duckity is divided into one or more applications within your account. You create one application
-per project in which you want to integrate Duckity. To create a new application, press on the
-yellow **+ New Application** button at the top right corner of the screen.
+Duckity is divided into one or more applications within your account. You create one application per
+project in which you want to integrate Duckity. To create a new application, press on the yellow **+
+New Application** button at the top right corner of the screen.
Once you've set the name of it, press on the **✓ Create Application** button. Congratulations! You
now have your first application created.
- Additionally, in case your application is a web app, you can set up the CORS
- origins to fit your application's origins. For example, if you'll be
- protecting the login form of your app hosted at `https://example.com/login`,
- add `https://example.com` (scheme and host, no path) to your app's origins. If
- your application is not a web app, you can leave the CORS origins empty.
+ Additionally, in case your application is a web app, you can set up the CORS origins to fit your
+ application's origins. For example, if you'll be protecting the login form of your app hosted at
+ `https://example.com/login`, add `https://example.com` (scheme and host, no path) to your app's
+ origins. If your application is not a web app, you can leave the CORS origins empty.
## Create a Protection Profile [step]
@@ -35,15 +34,14 @@ Each application needs at least one protection profile to work. To create your f
**+ Create Protection Profile** button.
Set the name to the name of the feature you'll be protecting, like "Sign-up Form" or "Add Friend".
-This is for you to recognize it later, it won't be displayed to your users. Once you're done
-filling it, press on **✓ Create Protection Profile**.
+This is for you to recognize it later, it won't be displayed to your users. Once you're done filling
+it, press on **✓ Create Protection Profile**.
Copy your protection profile's ID using the button at the top right corner, that's all you'll need
to integrate it into your application.
- Don't worry about all the settings displayed for now, you'll learn to tune
- them later.
+ Don't worry about all the settings displayed for now, you'll learn to tune them later.
## Install an SDK [step]
@@ -53,35 +51,21 @@ server-side SDK.
Duckity has multiple official SDKs for you to plug and play.
-import {
- SiC,
- SiJavascript,
- SiPython,
- SiRust,
- SiReact,
-} from "@icons-pack/react-simple-icons";
+import { SiC, SiJavascript, SiPython, SiRust, SiReact } from "@icons-pack/react-simple-icons";
- }>
- Integrate client-side code with Duckity using the JavaScript SDK.
-
- }>
- Integrate client-side code with Duckity using the React SDK.
-
- }>
- Integrate client-side code with Duckity using the C SDK.
-
- }>
- Integrate client-side code with Duckity using the Python SDK.
-
- }>
- Integrate client-side code with Duckity using the Rust SDK. Uses GMP via
- `rug`.
-
- }>
- Integrate client-side code with Duckity using the Rust SDK. Uses
- `num-bigint-dig`.
-
+ }>
+ 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.
+
In case we don't have a client SDK for your language yet, you can use the C SDK. It's written in
@@ -90,10 +74,6 @@ decoding in a simple API. You may also find the following guide useful:
import { FileCodeCorner } from "lucide-react";
-}
- href="/guides/write-your-own-sdk"
->
- Learn how to write your own SDK in any language.
+} href="/guides/write-your-own-sdk">
+ Learn how to write your own SDK in any language.
diff --git a/content/sdks/c.mdx b/content/sdks/c.mdx
deleted file mode 100644
index acd1fad..0000000
--- a/content/sdks/c.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: C
-icon: SiC
----
\ No newline at end of file
diff --git a/content/sdks/go.mdx b/content/sdks/go.mdx
deleted file mode 100644
index dbaa0fe..0000000
--- a/content/sdks/go.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Go
-icon: SiGo
----
\ No newline at end of file
diff --git a/content/sdks/javascript.mdx b/content/sdks/javascript.mdx
index da385ef..a9e2e68 100644
--- a/content/sdks/javascript.mdx
+++ b/content/sdks/javascript.mdx
@@ -1,20 +1,20 @@
---
-title: JavaScript (Client-Side)
+title: JavaScript
description: Learn how to integrate Duckity into your web application.
icon: SiJavascript
---
-Welcome to the Duckity JavaScript + WASM SDK documentation! This guide will teach you how to
-install and set up the SDK in no time.
+Welcome to the Duckity JavaScript SDK documentation! This guide will teach you how to install and
+set up the SDK in no time.
The following SDKs depend on this one and provide specialized wrappers for different frameworks:
import { SiReact } from "@icons-pack/react-simple-icons";
- }>
- Integrate client-side code with Duckity using the React SDK.
-
+ }>
+ Integrate client-side code with Duckity using the React SDK.
+
## Quick Start
@@ -36,38 +36,37 @@ Once you got those ready, follow these steps to get things running on your clien
Follow these steps depending on your application:
-
- Run the following line in your terminal to install the Duckity SDK.
+
+ Run the following line in your terminal to install the Duckity SDK.
- ```package-install
- @duckity/js
- ```
+ ```package-install
+ @duckity/js
+ ```
- Then import it in your code:
+ Then import it in your code:
- ```ts
- import duckity from "@duckity/js";
- ```
+ ```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.
-
-
- If you're using the SDK from a static site, import it using a CDN like [esm.sh](https://esm.sh/)
- instead.
+ ```html
+
+ ```
+
- // Using UNPKG
- import duckity from "https://unpkg.com/@duckity/js";
-
- ```
-
-
### Solve a Challenge [step]
@@ -76,55 +75,110 @@ Once you have the SDK installed, you can request a challenge whenever you need i
`duckity.solve()`.
```ts
-const PROTECTION_PROFILE_ID: string = "";
-// ---cut---
import duckity from "@duckity/js";
-let solution = await duckity.solve(PROTECTION_PROFILE_ID);
+/// Slow! Runs in a web worker, yet don't let it block code below unless necessary.
+let solution: string = await duckity.solve(PROTECTION_PROFILE_ID);
```
-
- Hover over the code to see the type definitions.
-
+### Validate a Challenge [step]
+
+Send the solution token to your backend server any way you want. A header or parameter in a JSON
+field will work.
+
+Once in the server, you'll need the client's IP, their submitted solution token, the protection
+profile's ID, and the application secret.
+
+```ts
+import * as duckity from "@duckity/js";
+
+let applicationSecret: string;
+let protectionProfileId: string;
+let clientIp: string;
+let solution: string;
+
+let isValid = await duckity.validate(solution, clientIp, applicationSecret, protectionProfileId);
+
+if (isValid) {
+ // Keep processing the request.
+} else {
+ // Return an error to the client.
+}
+```
+
+That's it! Now Duckity is protecting your endpoint. You can customize the behavior in the
+[Duckity Dashboard](https://app.duckity.com).
## Advanced Usage
-Solving a challenge on request works well for simple setups. However, both security and UX can be
-greatly improved changing a few settings and planning when to solve challenges.
+Solving a challenge on request works well for simple setups. However, UX can be greatly improved
+changing a few settings and planning when to solve challenges.
-If your challenges do not require threat correlation keys (set up in the protection profile's
-settings), issue the challenge as soon as possible. Note, however, that the challenge
+### Asynchronous Challenge Solving
-### Threat Correlation Keys
+The challenge does not need to wait for the user to finish filling up a form or completing an action
+to be issued. When you can guess the user will need a solution token, it is a good idea to start
+computing it before the user needs it.
-To pass threat correlation keys when issuing a challenge, set them in the `options` argument of
-`duckity.solve()`.
+For example, if the user opens a login page, you can infer the user will need a challenge solved to
+be able to attempt to log in. You can solve a challenge beforehand so that the user does not need to
+wait.
-```ts
-const PROTECTION_PROFILE_ID: string = "py83YHkXV6ZpIsJZGVxzS";
-// ---cut---
-import duckity from "@duckity/js";
+```html
+
+
+
+
+
+
+
+
+
+
```
-### Using On Self-Hosted Ducklings
+
+ Solution tokens expire after the time set in the dashboard. In case of failure to validate, try
+ refreshing the challenge (calling `duckity.solve()` again).
+
-Self-hosted ducklings are hosted at a different domain from Duckity-hosted ducklings. To point it
-to a custom domain, change the following setting:
+## Contributing & License
-```ts
-const PROTECTION_PROFILE_ID: string = "py83YHkXV6ZpIsJZGVxzS";
-// ---cut---
-import duckity from "@duckity/js";
+All contributions are welcome to the SDK. Whether it's bug fixes, suggestions, new features,
+documentation updates, or fixing a typo, if you think you can make this SDK better, feel free to
+make a pull request in the [GitHub repository](https://github.com/duckity-com/sdks).
-let solution = await duckity.solve(
- PROTECTION_PROFILE_ID,
- {
- api: "https://quack.duckity.com",
- }
-);
-```
-
-The default value for the `api` parameter is `"https://quack.duckity.com"`, which points to
-Duckity's hosted duckling. Change the domain name to your duckling's and you'll be ready to go.
+This SDK is licensed under the permissive MIT License, and so will be all contributions to the SDK.
diff --git a/content/sdks/python.mdx b/content/sdks/python.mdx
index a71e9fb..e36cbfa 100644
--- a/content/sdks/python.mdx
+++ b/content/sdks/python.mdx
@@ -1,4 +1,451 @@
---
title: Python 3
icon: SiPython
----
\ No newline at end of file
+---
+
+Welcome to the Duckity Python SDK documentation! This guide will teach you how to install and set up
+the SDK in no time.
+
+The SDK is async-first. To call it from synchronous code, wrap the calls in `asyncio.run()` or your
+event loop's version of it.
+
+
+ All SDKs are fully compatible with each other. Solution tokens generated in one SDK will be
+ validated just fine in other SDKs, including this one.
+
+
+## Quick Start
+
+Before you can integrate Duckity into your application, you'll need to have the following:
+
+1. An application,
+2. At least one protection profile created in that application, and
+3. The ID of the protection profiles to use
+
+If you're missing either of those, head over to the [Duckity Dashboard](https://app.duckity.com) or
+read the [Quick Start](/quick-start) guide to learn how to set those up.
+
+Once you got those ready, follow these steps to get things running on your client:
+
+### Install the SDK [step]
+
+Install the SDK from PyPI using your favorite package manager:
+
+{/* prettier-ignore */}
+
+
+ ```sh
+ pip install duckity
+ ```
+
+
+ ```sh
+ uv add duckity
+ ```
+
+
+ ```sh
+ poetry add duckity
+ ```
+
+
+
+### Solve a Challenge [step]
+
+Solving a challenge only requires a 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.
+
+
+
+
+### Validate a Solution [step]
+
+Validation is done server-side. You can send the solution token to your server any way you want; A
+JSON field or an HTTP header is usually convenient.
+
+To validate a solution token, you'll need 4 things:
+
+1. The solution token,
+2. The IP of the client that submitted the solution,
+3. The application's secret, and
+4. The protection profile's ID.
+
+Once you have them, you can validate a solution token as follows:
+
+
+
+ ```py
+ import duckity
+
+ solution: str
+ client_ip: str
+ application_secret: str
+ protection_profile_id: str
+
+ is_valid: bool = await duckity.validate(
+ solution, client_ip, application_secret, protection_profile_id
+ )
+ ```
+
+
+ ```py
+ import asyncio
+
+ import duckity
+
+ solution: str
+ client_ip: str
+ application_secret: str
+ protection_profile_id: str
+
+ 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.
+
+## Advanced Usage
+
+Solving a challenge on demand works well for simple setups. However, UX can be greatly improved
+changing a few settings and planning when to solve challenges.
+
+### Asynchronous Challenge Solving
+
+The challenge does not need to wait for the user to finish filling up a form or completing an action
+to be issued. When you can guess the user will need a solution token, it is a good idea to start
+computing it before the user needs it.
+
+For example, if the user is logging in to a backend service via a CLI, you can fetch and solve a
+challenge while the user is filling up their username and password. For example:
+
+```py
+import asyncio
+
+import getpass
+
+import duckity
+
+
+async def login():
+ solution_task = asyncio.create_task(duckity.solve(PROTECTION_PROFILE_ID))
+
+ email = await asyncio.to_thread(input, "Enter your email: ")
+ password = await asyncio.to_thread(getpass.getpass, "Enter your password (hidden): ")
+
+ print("Logging you in...")
+
+ solution = await solution_task
+
+ # Log the user in here...
+
+
+asyncio.run(login())
+```
+
+## Integrations
+
+Duckity's Python SDK works anywhere out of the box. These examples show how to integrate it to some
+common frameworks and tools.
+
+### FastAPI
+
+```py lineNumbers
+import os
+
+import dotenv
+
+import duckity
+
+from fastapi import FastAPI, Request, HTTPException
+
+from pydantic import BaseModel
+
+
+dotenv.load_dotenv(override=True)
+
+
+PROTECTION_PROFILE_ID = os.environ["DUCKITY_PROTECTION_PROFILE_ID"]
+APPLICATION_SECRET = os.environ["DUCKITY_APPLICATION_SECRET"]
+
+
+app = FastAPI()
+
+
+class RequestBodySchema(BaseModel):
+ solution: str
+
+
+@app.post("/protected")
+async def protected(request: Request, payload: RequestBodySchema):
+ is_valid = await duckity.validate(
+ payload.solution,
+ request.client.host, # Make sure to take into account X-Forwarded-For if behind a reverse proxy.
+ APPLICATION_SECRET,
+ PROTECTION_PROFILE_ID
+ )
+
+ if not is_valid:
+ raise HTTPException(
+ status_code=400,
+ detail="The provided solution token was invalid.",
+ )
+
+ return {
+ "message": "This is protected!"
+ }
+```
+
+### Flask
+
+```py lineNumbers
+import os
+
+import dotenv
+
+import duckity
+
+from flask import Flask, request, jsonify
+
+
+dotenv.load_dotenv(override=True)
+
+
+app = Flask(__name__)
+
+
+PROTECTION_PROFILE_ID = os.environ["DUCKITY_PROTECTION_PROFILE_ID"]
+APPLICATION_SECRET = os.environ["DUCKITY_APPLICATION_SECRET"]
+
+
+@app.post("/protected")
+async def protected():
+ solution = request.json.get("solution")
+
+ if not solution:
+ return jsonify({"error": "Missing solution"}), 400
+
+ client_ip = request.remote_addr # Make sure to take into account X-Forwarded-For if behind a reverse proxy.
+
+ is_valid = await duckity.validate(
+ solution,
+ client_ip,
+ APPLICATION_SECRET,
+ PROTECTION_PROFILE_ID,
+ )
+
+ if not is_valid:
+ return jsonify({"error": "Invalid solution"}), 403
+
+ return jsonify({"message": "This is protected!"})
+```
+
+### Django
+
+```py lineNumbers
+import json
+
+import duckity
+
+from django.conf import settings
+from django.http import JsonResponse
+
+
+async def protected(request):
+ if request.method != "POST":
+ return JsonResponse(
+ {"detail": "Method not allowed."},
+ status=405,
+ )
+
+ try:
+ body = json.loads(request.body)
+ solution = body["solution"]
+ except (json.JSONDecodeError, KeyError, TypeError):
+ return JsonResponse(
+ {"detail": "Invalid request body."},
+ status=400,
+ )
+
+ # Make sure to take into account X-Forwarded-For if behind a reverse proxy.
+ client_ip = request.META["REMOTE_ADDR"]
+
+ is_valid = await duckity.validate(
+ solution,
+ client_ip,
+ settings.DUCKITY_APPLICATION_SECRET,
+ settings.DUCKITY_PROTECTION_PROFILE_ID,
+ )
+
+ if not is_valid:
+ return JsonResponse(
+ {"detail": "The provided solution token was invalid."},
+ status=400,
+ )
+
+ return JsonResponse({
+ "message": "This is protected!"
+ })
+```
+
+### Django Ninja
+
+```py lineNumbers
+from ninja import NinjaAPI, Schema
+from ninja.errors import HttpError
+
+from django.conf import settings
+
+
+api = NinjaAPI()
+
+
+class RequestBodySchema(Schema):
+ solution: str
+
+
+@api.post("/protected")
+async def protected(request, payload: RequestBodySchema):
+ # Make sure to take into account X-Forwarded-For if behind a reverse proxy.
+ client_ip = request.META["REMOTE_ADDR"]
+
+ is_valid = await duckity.validate(
+ payload.solution,
+ client_ip,
+ settings.DUCKITY_APPLICATION_SECRET,
+ settings.DUCKITY_PROTECTION_PROFILE_ID,
+ )
+
+ if not is_valid:
+ raise HttpError(400, "The provided solution token was invalid.")
+
+ return {
+ "message": "This is protected!"
+ }
+```
+
+### Django Rest Framework
+
+```py lineNumbers
+import duckity
+
+from django.conf import settings
+
+from rest_framework import serializers, status
+from rest_framework.views import APIView
+from rest_framework.response import Response
+
+
+class RequestBodySerializer(serializers.Serializer):
+ solution = serializers.CharField()
+
+
+class ProtectedView(APIView):
+ async def post(self, request):
+ serializer = RequestBodySerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ # Make sure to take into account X-Forwarded-For if behind a reverse proxy.
+ client_ip = request.META["REMOTE_ADDR"]
+
+ is_valid = await duckity.validate(
+ serializer.validated_data["solution"],
+ client_ip,
+ settings.DUCKITY_APPLICATION_SECRET,
+ settings.DUCKITY_PROTECTION_PROFILE_ID,
+ )
+
+ if not is_valid:
+ return Response(
+ {"detail": "The provided solution token was invalid."},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ return Response({"message": "This is protected!"})
+```
+
+### Strawberry
+
+```py lineNumbers
+import os
+
+import dotenv
+
+import duckity
+
+import strawberry
+
+
+dotenv.load_dotenv(override=True)
+
+
+PROTECTION_PROFILE_ID = os.environ["DUCKITY_PROTECTION_PROFILE_ID"]
+APPLICATION_SECRET = os.environ["DUCKITY_APPLICATION_SECRET"]
+
+
+@strawberry.type
+class Query:
+ pass
+
+
+@strawberry.type
+class ProtectedResource:
+ message: str
+
+
+@strawberry.type
+class Mutation:
+ @strawberry.mutation
+ async def protected(self, info: strawberry.Info, solution: str) -> ProtectedResource:
+ # Make sure to take into account X-Forwarded-For if behind a reverse proxy.
+ client_ip = info.context["request"].META["REMOTE_ADDR"]
+
+ is_valid = await duckity.validate(
+ solution,
+ client_ip,
+ DUCKITY_APPLICATION_SECRET,
+ DUCKITY_PROTECTION_PROFILE_ID,
+ )
+
+ if not is_valid:
+ raise Exception("The provided solution was not valid.")
+
+ return ProtectedResource(message="This is protected!")
+
+
+schema = strawberry.Schema(query=Query, mutation=Mutation)
+```
+
+## Contributing & License
+
+All contributions are welcome to the SDK. Whether it's bug fixes, suggestions, new features,
+documentation updates, or fixing a typo, if you think you can make this SDK better, feel free to
+make a pull request in the [GitHub repository](https://github.com/duckity-com/sdks).
+
+This SDK is licensed under the permissive MIT License, and so will be all contributions to the SDK.
diff --git a/content/sdks/react.mdx b/content/sdks/react.mdx
index 2bd3153..9122014 100644
--- a/content/sdks/react.mdx
+++ b/content/sdks/react.mdx
@@ -1,4 +1,210 @@
---
title: React
icon: SiReact
----
\ No newline at end of file
+---
+
+Welcome to the React SDK documentation! This is a wrapper over the JavaScript SDK for ease of use in
+React components. In case you're looking for the JavaScript SDK, head over to the JavaScript SDK
+guide.
+
+import { SiJavascript } from "@icons-pack/react-simple-icons";
+
+
+ }>
+ Integrate client and server-side code with Duckity using the JavaScript/TypeScript SDK.
+
+
+
+## Quick Start
+
+Before you can integrate Duckity into your application, you'll need to have the following:
+
+1. An application,
+2. CORS origins set up pointing to your application's origin(s),
+3. At least one protection profile created in that application, and
+4. The ID of the protection profiles to use
+
+If you're missing either of those, head over to the [Duckity Dashboard](https://app.duckity.com) or
+read the [Quick Start](/quick-start) guide to learn how to set those up.
+
+Once you got those ready, follow these steps to get things running on your client:
+
+### Install the SDK [step]
+
+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";
+```
+
+### Use the Hook [step]
+
+Use the hook as follows:
+
+
+
+ ```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.
+
+### Get a Challenge Solution [step]
+
+`useChallenge()` returns an object with multiple properties and functions to help you react to state
+changes. Reading the solution is doable in two different ways.
+
+1. Reading `useChallenge().solution: string | undefined`, and
+2. Calling `useChallenge().wait(): Promise`.
+
+Most of the time, you'll likely want to call `useChallenge().wait()`.
+
+```tsx
+import { SubmitEvent } from "react";
+import { useChallenge } from "@duckity/react";
+
+function MyComponent() {
+ const duckity = useChallenge(...);
+
+ async function handleSubmit(e: SubmitEvent) {
+ e.preventDefault();
+
+ const solution: string = await duckity.wait();
+
+ // Submit the form to your backend.
+ }
+
+ return (
+
+ )
+}
+```
+
+In case you need to submit more than one solution (e.g. because the user can perform multiple
+protected actions without reloading the page), call `duckity.refresh()` once you have used the
+solution token. It will fetch a new challenge and solve it in the background.
+
+## `UseChallengeResult`
+
+Setting up the hook and calling `wait()` works well for simple setups. However, once your component
+scales, you may want to use the rest of the properties returned by `useChallenge()`.
+
+",
+ required: true,
+ },
+ status: {
+ description: "The current status of the challenge hook.",
+ type: '"solving" | "solved" | "error"',
+ required: true,
+ },
+ solution: {
+ description: "The encoded solution string, if any.",
+ type: "string",
+ },
+ error: {
+ description: "An error, if any occurred while fetching or solving a challenge.",
+ type: "any",
+ },
+ }}
+/>
+
+## Contributing & License
+
+All contributions are welcome to the SDK. Whether it's bug fixes, suggestions, new features,
+documentation updates, or fixing a typo, if you think you can make this SDK better, feel free to
+make a pull request in the [GitHub repository](https://github.com/duckity-com/sdks).
+
+This SDK is licensed under the permissive MIT License, and so will be all contributions to the SDK.
diff --git a/content/sdks/rust-no-std.mdx b/content/sdks/rust-no-std.mdx
deleted file mode 100644
index 7eed0a8..0000000
--- a/content/sdks/rust-no-std.mdx
+++ /dev/null
@@ -1,87 +0,0 @@
----
-title: Rust (No STD)
-icon: SiRust
----
-
-Welcome to the Rust (no STD) SDK documentation! This crate does not require the standard library to
-compile. It's considerably slower than the STD-enabled SDK, up to x4 slower. If you can use the STD
-crate, choose it.
-
-## Quick Start
-
-Before you can integrate Duckity into your application, you'll need to have the following:
-
-1. An application,
-2. At least one protection profile created in that application, and
-3. The ID of the protection profile(s) to use
-
-If you're missing either of those, head over to the [Duckity Dashboard](https://app.duckity.com) or
-read the [Quick Start](/quick-start) guide to learn how to set those up.
-
-Once you got those ready, follow these steps to get things running on your client:
-
-### Install the SDK [step]
-
-To install the SDK in your project, run the following command in your shell:
-
-```sh
-cargo install duckity-core
-```
-
-### Get a Challenge [step]
-
-This crate does not include utilities to fetch challenges. To get a challenge, make a POST HTTP
-request like the following:
-
-```http
-GET /v1/challenges/{protection-profile-id}/issue HTTP/1.1
-Host: quack.duckity.com
-Accept: application/json
-Content-Type: application/json
-
-{}
-```
-
-In case you want to specify threat correlation keys, pass them in the object under `keys`, as
-follows:
-
-```json
-{
- "keys": {
- "key1": "val1",
- "key2": "val2"
- }
-}
-```
-
-The response on success will look like follows:
-
-```http
-HTTP/1.1 200 OK
-Content-Type: application/json
-X-RateLimit-Next-In: 1000
-X-RateLimit-Resets-In: 1000
-X-RateLimit-Remaining: 0
-X-RateLimit-Penalty-Resets-In: 0
-
-{
- "challenge": ""
-}
-```
-
-### Solve a Challenge [step]
-
-Once you have a challenge string, use the following methods to solve a challenge:
-
-```rs
-fn main() {
- let original = String::from("");
-
- let challenge = duckity_core::decode(&original).unwrap();
- let solution = duckity_core::solve(&challenge);
- let solution = duckity_core::encode(&original, &solution).unwrap();
-}
-```
-
-`duckity_core::solve()` is CPU-intensive, take appropriate measures if it may block concurrent
-processing (e.g. a UI thread).
diff --git a/content/sdks/rust-std.mdx b/content/sdks/rust-std.mdx
deleted file mode 100644
index b64100d..0000000
--- a/content/sdks/rust-std.mdx
+++ /dev/null
@@ -1,90 +0,0 @@
----
-title: Rust (STD)
-icon: SiRust
----
-
-Welcome to the Rust (with STD) SDK documentation! As the name says, and unlike the no-STD SDK, this
-SDK requires the standard library to compile. In exchange, it's about 3 times faster than the
-no-STD SDK.
-
-## Quick Start
-
-Before you can integrate Duckity into your application, you'll need to have the following:
-
-1. An application,
-2. At least one protection profile created in that application, and
-3. The ID of the protection profile(s) to use
-
-If you're missing either of those, head over to the [Duckity Dashboard](https://app.duckity.com) or
-read the [Quick Start](/quick-start) guide to learn how to set those up.
-
-Once you got those ready, follow these steps to get things running on your client:
-
-### Install the SDK [step]
-
-To install the SDK in your project, run the following command in your shell:
-
-```sh
-cargo install duckity
-```
-
-You'll also need Tokio to use it (`reqwest` depends on it).
-
-
- In case you cannot use Tokio, the `async-compat` crate may help. Using it is
- untested, and out of scope for this documentation. You may also skip getting
- challenges using the SDK and only use the `duckity` create to solve
- manually-fetched challenges.
-
-
-### Get and Solve a Challenge [step]
-
-Once you have `duckity` installed, you can start getting challenges with `duckity::get()`.
-
-```rs
-const PROTECTION_PROFILE_ID: &'static str = "";
-
-#[tokio::main]
-async fn main() -> anyhow::Result<()> {
- let challenge: String = duckity::get(PROTECTION_PROFILE_ID).await?;
- let solution: String = tokio::task::spawn_blocking(move || duckity::solve(&challenge))??;
-
- println!("{solution}");
-
- Ok(())
-}
-```
-
-Don't forget to run `duckity::solve()` in `tokio::task::spawn_blocking()`. Calling
-`duckity::solve()` directly from your async context will block the runtime's thread until the call
-is done. `tokio::task::spawn_blocking()` is available under the `rt` tokio feature.
-
-## Advanced Usage
-
-Threat correlation and self-hosted endpoints are also supported by this crate.
-
-### Threat Correlation Keys
-
-To specify threat correlation keys when getting a challenge, use `ChallengeGetter::key()`. For
-example:
-
-```rs
-let challenge = duckity::get(PROTECTION_PROFILE_ID)
- .key("key1", key1)
- .key("key2", key2)
- .await?;
-```
-
-### Using On Self-Hosted Ducklings
-
-Self-hosted ducklings are hosted at a different domain from Duckity-hosted ducklings. To point it
-to a custom domain, use `ChallengeGetter::base_url()`:
-
-```rs
-let challenge = duckity::get(PROTECTION_PROFILE_ID)
- .base_url("https://quack.duckity.com") // Without trailing slash
- .await?;
-```
-
-The default value for the `api` parameter is `"https://quack.duckity.com"`, which points to
-Duckity's hosted duckling. Change the domain name to your duckling's and you'll be ready to go.
diff --git a/content/sdks/rust.mdx b/content/sdks/rust.mdx
new file mode 100644
index 0000000..1e4302f
--- /dev/null
+++ b/content/sdks/rust.mdx
@@ -0,0 +1,364 @@
+---
+title: Rust
+icon: SiRust
+---
+
+Welcome to the Rust SDK documentation! This guide will teach you how to install and set up the SDK
+in no time.
+
+This SDK uses `reqwest`, which depends on Tokio. If you want to use a different async runtime,
+you'll have to disable the `std` feature of the crate and implement your own interactions with the
+Duckling API. Note that disabling the `std` feature still requires a global allocator.
+
+This crate also has full documentation available at [docs.rs/duckity](https://docs.rs/duckity) plus
+all the type introspection that this document does not provide. It may be useful to check that out
+in case you want to use the `core` module later on.
+
+## Quick Start
+
+Before you can integrate Duckity into your application, you'll need to have the following:
+
+1. An application,
+2. At least one protection profile created in that application, and
+3. The ID of the protection profiles to use
+
+If you're missing either of those, head over to the [Duckity Dashboard](https://app.duckity.com) or
+read the [Quick Start](/quick-start) guide to learn how to set those up.
+
+Once you got those ready, follow these steps to get things running on your client:
+
+### Install the SDK [step]
+
+Run the following cargo command in your terminal to add the `duckity` crate to your project:
+
+```sh
+cargo add duckity
+```
+
+### Solve a Challenge [step]
+
+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.
+
+
+ The CPU-intensive part of solving the challenge is done in a separate thread via
+ `tokio::task::spawn_blocking()`. Your async runtime will not be blocked.
+
+
+### Validate a Challenge [step]
+
+Validation is done server-side. You can send the solution token to your server any way you want; A
+JSON field or an HTTP header is usually convenient.
+
+To validate a solution token, you'll need 4 things:
+
+1. The solution token,
+2. The IP of the client that submitted the solution,
+3. The application's secret, and
+4. The protection profile's ID.
+
+Once you have them, you can validate a solution token as follows:
+
+```rs
+let is_valid: bool = duckity::validate(
+ solution,
+ client_ip,
+ APPLICATION_SECRET,
+ PROTECTION_PROFILE_ID
+)
+.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.
+
+## Advanced Usage
+
+Solving a challenge on demand works well for simple setups. However, UX can be greatly improved by
+planning when to solve challenges. Additionally, you can access the low-level APIs in the
+`duckity::core::` module to customize your processing of the challenges.
+
+### Asynchronous Challenge Solving
+
+The challenge does not need to wait for the user to finish filling up a form or completing an action
+to be issued. When you can guess the user will need a solution token, it is a good idea to start
+computing it before the user needs it.
+
+For example, if the user is logging in to a backend service via a CLI, you can fetch and solve a
+challenge while the user is filling up their username and password. For example:
+
+```rs
+use std::io::{self, Write};
+
+const PROTECTION_PROFILE_ID: &str = "";
+
+struct Credentials {
+ email: String,
+ password: String,
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ let solution_task = tokio::spawn(duckity::solve(PROTECTION_PROFILE_ID).into_future());
+
+ let credentials = tokio::task::spawn_blocking(|| {
+ let mut email = String::new();
+ let mut password = String::new();
+
+ print!("Enter your email: ");
+ io::stdout().flush()?;
+ io::stdin().read_line(&mut email)?;
+
+ print!("Enter your password: ");
+ io::stdout().flush()?;
+ io::stdin().read_line(&mut password)?;
+
+ anyhow::Ok(Credentials { email, password })
+ })
+ .await??;
+
+ let solution = solution_task.await??;
+
+ Ok(())
+}
+```
+
+### `duckity::core::` Module
+
+This module is available even without the `std` feature. It contains all the internals that
+`duckity::solve()` uses to decode, solve, and encode challenges and solutions, and is useful when
+you want to read challenge metadata or customize the way challenges are solved.
+
+The three functions provided by this module are the following:
+
+1. `duckity::core::decode(&str) -> Result`
+2. `duckity::core::solve(&Challenge) -> Solution`
+3. `duckity::core::encode(&str, &Solution) -> Result`
+
+Additionally, two struct types are provided:
+
+1. `Challenge`
+2. `Solution`
+
+The `Challenge` struct contains 2 public fields and 1 method:
+
+- `Challenge::id` - The challenge's unique ID.
+- `Challenge::ip` - The IP of the client this challenge was issued for. `IpAddr` under the `std`
+ feature flag, `String` when it's disabled.
+- `Challenge::hardness()` - The hardness of the challenge.
+
+The `Solution` struct is opaque. It can only be passed to `duckity::core::encode()` for encoding
+into a solution token.
+
+### Using a Different Async Runtime
+
+You can customize the asynchronous runtime by disabling the `std` feature flag and implementing the
+fetching and validating yourself. The decoding, solving, and encoding functions are all provided
+inside the `duckity::core::` module and are still available without the `std` feature flag under
+environments with `alloc`.
+
+The [`async-compat`](//docs.rs/async-compat) crate may make migration to another runtime extremely
+simple. For example, the following snippet shows how to solve and validate a challenge with
+[`smol`](//docs.rs/smol).
+
+```rs
+use async_compat::CompatExt;
+
+const PROTECTION_PROFILE_ID: &str = "";
+
+fn main() -> anyhow::Result<()> {
+ smol::block_on(async {
+ let solution = duckity::solve(PROTECTION_PROFILE_ID).into_future().compat().await?;
+
+ Ok(())
+ })
+}
+```
+
+## Integrations
+
+This SDK integrates easily with multiple frameworks and libraries. The following examples show how
+to integrate with a few common libraries.
+
+### Axum
+
+```rs lineNumbers
+use std::net::SocketAddr;
+
+use axum::extract::ConnectInfo;
+use axum::response::IntoResponse;
+use axum::{Json, Router, routing};
+use reqwest::StatusCode;
+use serde::Deserialize;
+use tokio::net::TcpListener;
+
+// In an actual application, make these two configurable. `clap` is a good tool for that.
+const APPLICATION_SECRET: &str = "";
+const PROTECTION_PROFILE_ID: &str = "";
+
+#[tokio::main]
+async fn main() {
+ let router = Router::new().route("/protected", routing::post(handler));
+
+ let listener = TcpListener::bind("0.0.0.0:8000").await.unwrap();
+
+ axum::serve(
+ listener,
+ router.into_make_service_with_connect_info::(),
+ )
+ .await
+ .unwrap();
+}
+
+#[derive(Deserialize)]
+struct ProtectedRequestPayload {
+ solution: String,
+}
+
+async fn handler(
+ // If behind a reverse proxy, use X-Forwarded-For instead. Make sure it's not spoofable.
+ ConnectInfo(addr): ConnectInfo,
+ Json(payload): Json,
+) -> impl IntoResponse {
+ let is_valid = duckity::validate(
+ payload.solution,
+ addr.ip(),
+ APPLICATION_SECRET,
+ PROTECTION_PROFILE_ID,
+ )
+ .await
+ .unwrap();
+
+ if is_valid {
+ (StatusCode::OK, Json("This is protected!"))
+ } else {
+ (
+ StatusCode::BAD_REQUEST,
+ Json("The provided solution token was invalid."),
+ )
+ }
+}
+```
+
+### Warp
+
+```rs lineNumbers
+use std::convert::Infallible;
+use std::net::SocketAddr;
+
+use serde::Deserialize;
+use warp::{Filter, Reply};
+
+// In an actual application, make these two configurable. `clap` is a good tool for that.
+const APPLICATION_SECRET: &str = "";
+const PROTECTION_PROFILE_ID: &str = "";
+
+#[tokio::main]
+async fn main() {
+ let protected = warp::path("protected")
+ .and(warp::post())
+ .and(warp::addr::remote())
+ .and(warp::body::json::())
+ .and_then(handler);
+
+ warp::serve(protected)
+ .run(([0, 0, 0, 0], 8000))
+ .await;
+}
+
+#[derive(Deserialize)]
+struct ProtectedRequestPayload {
+ solution: String,
+}
+
+async fn handler(
+ // If behind a reverse proxy, use X-Forwarded-For instead. Make sure it cannot be spoofed.
+ addr: Option,
+ payload: ProtectedRequestPayload,
+) -> Result {
+ let addr = addr.expect("remote address unavailable");
+
+ let is_valid = duckity::validate(
+ payload.solution,
+ addr.ip(),
+ APPLICATION_SECRET,
+ PROTECTION_PROFILE_ID,
+ )
+ .await
+ .unwrap();
+
+ if is_valid {
+ Ok(warp::reply::with_status(
+ warp::reply::json(&"This is protected!"),
+ warp::http::StatusCode::OK,
+ ))
+ } else {
+ Ok(warp::reply::with_status(
+ warp::reply::json(&"The provided solution token was invalid."),
+ warp::http::StatusCode::BAD_REQUEST,
+ ))
+ }
+}
+```
+
+### Actix Web
+
+```rs lineNumbers
+use std::net::SocketAddr;
+
+use actix_web::web::Json;
+use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, post};
+use serde::Deserialize;
+
+// In an actual application, make these two configurable. `clap` is a good tool for that.
+const APPLICATION_SECRET: &str = "";
+const PROTECTION_PROFILE_ID: &str = "";
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+ HttpServer::new(|| App::new().service(protected))
+ .bind(("0.0.0.0", 8000))?
+ .run()
+ .await
+}
+
+#[derive(Deserialize)]
+struct ProtectedRequestPayload {
+ solution: String,
+}
+
+#[post("/protected")]
+async fn protected(req: HttpRequest, payload: Json) -> impl Responder {
+ // If behind a reverse proxy, use X-Forwarded-For instead.
+ // Make sure it cannot be spoofed.
+ let addr: SocketAddr = req.peer_addr().expect("remote address unavailable");
+
+ let is_valid = duckity::validate(
+ payload.solution.clone(),
+ addr.ip(),
+ APPLICATION_SECRET,
+ PROTECTION_PROFILE_ID,
+ )
+ .await
+ .unwrap();
+
+ if is_valid {
+ HttpResponse::Ok().json("This is protected!")
+ } else {
+ HttpResponse::BadRequest().json("The provided solution token was invalid.")
+ }
+}
+```
+
+## Contributing & License
+
+All contributions are welcome to the SDK. Whether it's bug fixes, suggestions, new features,
+documentation updates, or fixing a typo, if you think you can make this SDK better, feel free to
+make a pull request in the [GitHub repository](https://github.com/duckity-com/sdks).
+
+This SDK is licensed under the permissive MIT License, and so will be all contributions to the SDK.
diff --git a/package.json b/package.json
index 47e43f0..ee9d683 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@typescript/vfs": "^1.6.4",
"beautiful-mermaid": "^1.1.3",
"class-variance-authority": "^0.7.1",
+ "cn": "^0.2.4",
"cnfast": "^0.0.8",
"fumadocs-core": "16.12.1",
"fumadocs-mdx": "15.2.0",
diff --git a/src/components/mdx.tsx b/src/components/mdx.tsx
index 66eac15..e0d54c4 100644
--- a/src/components/mdx.tsx
+++ b/src/components/mdx.tsx
@@ -4,6 +4,7 @@ import Image from "next/image";
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';
export function getMDXComponents(components?: MDXComponents) {
return {
@@ -12,6 +13,7 @@ export function getMDXComponents(components?: MDXComponents) {
...TabsComponents,
...Twoslash,
Mermaid,
+ TypeTable,
...components,
} satisfies MDXComponents;
}
diff --git a/src/components/type-table.tsx b/src/components/type-table.tsx
new file mode 100644
index 0000000..038d485
--- /dev/null
+++ b/src/components/type-table.tsx
@@ -0,0 +1,183 @@
+'use client';
+
+import { ChevronDown } from 'lucide-react';
+import Link from 'fumadocs-core/link';
+import { cva } from 'class-variance-authority';
+import { cn } from '../lib/cn';
+import { type ComponentProps, type ReactNode, useEffect, useState } from 'react';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
+import { useTranslations } from '@fuma-translate/react';
+
+export interface ParameterNode {
+ name: string;
+ description: ReactNode;
+}
+
+export interface TypeNode {
+ /**
+ * Additional description of the field
+ */
+ description?: ReactNode;
+
+ /**
+ * type signature (short)
+ */
+ type: ReactNode;
+
+ /**
+ * type signature (full)
+ */
+ typeDescription?: ReactNode;
+
+ /**
+ * Optional `href` for the type
+ */
+ typeDescriptionLink?: string;
+
+ default?: ReactNode;
+
+ required?: boolean;
+ deprecated?: boolean;
+
+ /**
+ * a list of parameters info if the type is a function.
+ */
+ parameters?: ParameterNode[];
+
+ returns?: ReactNode;
+}
+
+const fieldVariants = cva('text-fd-muted-foreground not-prose pe-2');
+
+export function TypeTable({
+ id,
+ type,
+ className,
+ ...props
+}: { type: Record } & ComponentProps<'div'>) {
+ const t = useTranslations({ note: 'type table' });
+
+ return (
+