Checkpoint

This commit is contained in:
2026-09-05 01:06:01 -03:00
parent 8baf893a9e
commit 2b3fc987a9
17 changed files with 1413 additions and 303 deletions
+26
View File
@@ -0,0 +1,26 @@
{
"overrides": [
{
"files": [
"*.tsx",
"*.jsx",
"*.ts",
"*.js"
],
"options": {
"tabWidth": 2
}
},
{
"files": [
"*.mdx",
"*.md"
],
"options": {
"tabWidth": 4,
"proseWrap": "always",
"printWidth": 100
}
}
]
}
+3
View File
@@ -14,6 +14,7 @@
"@typescript/vfs": "^1.6.4", "@typescript/vfs": "^1.6.4",
"beautiful-mermaid": "^1.1.3", "beautiful-mermaid": "^1.1.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"cn": "^0.2.4",
"cnfast": "^0.0.8", "cnfast": "^0.0.8",
"fumadocs-core": "16.12.1", "fumadocs-core": "16.12.1",
"fumadocs-mdx": "15.2.0", "fumadocs-mdx": "15.2.0",
@@ -415,6 +416,8 @@
"clsx": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "clsx": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cn": ["[email protected]", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-OCjZtMeQfXbI4Es1+EIjkd77gvWzaE689gD8KhfexlqjClC06qR1MQBR+Z35ZMSPNEBWyHiItW1Soy0UvwNv9w=="],
"cnfast": ["[email protected]", "", { "bin": { "cnfast": "bin/cli.js" } }, "sha512-EjXKMfGfdwtV4AcNSQ6AwQaVzpC1B7IxeiwA3FlhTXz+YFlMKVi4c1JX9tgD2QOlahQXjB8KUXrBaYG+3v871Q=="], "cnfast": ["[email protected]", "", { "bin": { "cnfast": "bin/cli.js" } }, "sha512-EjXKMfGfdwtV4AcNSQ6AwQaVzpC1B7IxeiwA3FlhTXz+YFlMKVi4c1JX9tgD2QOlahQXjB8KUXrBaYG+3v871Q=="],
"collapse-white-space": ["[email protected]", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "collapse-white-space": ["[email protected]", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
+1 -1
View File
@@ -711,7 +711,7 @@ In TypeScript, this equals to
</Tabs> </Tabs>
#### `isPrimeForBase` [step] #### `is_prime_for_base` [step]
This function will test a number using Miller-Rabin for the specified base. By later testing with 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 different bases in `is_prime()`, we increase the likelyhood of a number being prime. The test works
+1 -4
View File
@@ -9,10 +9,7 @@
"---[CodeXml]Integrate---", "---[CodeXml]Integrate---",
"sdks/javascript", "sdks/javascript",
"sdks/react", "sdks/react",
"sdks/c",
"sdks/python", "sdks/python",
"sdks/rust-std", "sdks/rust"
"sdks/rust-no-std",
"sdks/go"
] ]
} }
+25 -45
View File
@@ -10,19 +10,18 @@ first time logging in, your account will be automatically created.
## Create an Application [step] ## Create an Application [step]
Duckity is divided into one or more applications within your account. You create one application Duckity is divided into one or more applications within your account. You create one application per
per project in which you want to integrate Duckity. To create a new application, press on the project in which you want to integrate Duckity. To create a new application, press on the yellow **+
yellow **+ New Application** button at the top right corner of the screen. 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 Once you've set the name of it, press on the **✓ Create Application** button. Congratulations! You
now have your first application created. now have your first application created.
<Callout> <Callout>
Additionally, in case your application is a web app, you can set up the CORS Additionally, in case your application is a web app, you can set up the CORS origins to fit your
origins to fit your application's origins. For example, if you'll be application's origins. For example, if you'll be protecting the login form of your app hosted at
protecting the login form of your app hosted at `https://example.com/login`, `https://example.com/login`, add `https://example.com` (scheme and host, no path) to your app's
add `https://example.com` (scheme and host, no path) to your app's origins. If origins. If your application is not a web app, you can leave the CORS origins empty.
your application is not a web app, you can leave the CORS origins empty.
</Callout> </Callout>
## Create a Protection Profile [step] ## 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. **+ Create Protection Profile** button.
Set the name to the name of the feature you'll be protecting, like "Sign-up Form" or "Add Friend". 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 This is for you to recognize it later, it won't be displayed to your users. Once you're done filling
filling it, press on **✓ Create Protection Profile**. 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 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. to integrate it into your application.
<Callout> <Callout>
Don't worry about all the settings displayed for now, you'll learn to tune Don't worry about all the settings displayed for now, you'll learn to tune them later.
them later.
</Callout> </Callout>
## Install an SDK [step] ## Install an SDK [step]
@@ -53,35 +51,21 @@ server-side SDK.
Duckity has multiple official SDKs for you to plug and play. Duckity has multiple official SDKs for you to plug and play.
import { import { SiC, SiJavascript, SiPython, SiRust, SiReact } from "@icons-pack/react-simple-icons";
SiC,
SiJavascript,
SiPython,
SiRust,
SiReact,
} from "@icons-pack/react-simple-icons";
<Cards> <Cards>
<Card href="/sdks/javascript" title="JavaScript" icon={<SiJavascript />}> <Card href="/sdks/javascript" title="JavaScript" icon={<SiJavascript />}>
Integrate client-side code with Duckity using the JavaScript SDK. Integrate Duckity into your application using the JavaScript SDK.
</Card> </Card>
<Card href="/sdks/react" title="React" icon={<SiReact />}> <Card href="/sdks/react" title="React" icon={<SiReact />}>
Integrate client-side code with Duckity using the React SDK. Integrate Duckity into your React app using the React SDK.
</Card> </Card>
<Card href="/sdks/c" title="C" icon={<SiC />}> <Card href="/sdks/python" title="Python 3" icon={<SiPython />}>
Integrate client-side code with Duckity using the C SDK. Integrate Duckity into your application using the Python SDK.
</Card> </Card>
<Card href="/sdks/python" title="Python 3" icon={<SiPython />}> <Card href="/sdks/rust" title="Rust" icon={<SiRust />}>
Integrate client-side code with Duckity using the Python SDK. Integrate Duckity into your application using the Rust SDK. Supports no-std environments.
</Card> </Card>
<Card href="/sdks/rust-std" title="Rust (STD)" icon={<SiRust />}>
Integrate client-side code with Duckity using the Rust SDK. Uses GMP via
`rug`.
</Card>
<Card href="/sdks/rust-no-std" title="Rust (No STD)" icon={<SiRust />}>
Integrate client-side code with Duckity using the Rust SDK. Uses
`num-bigint-dig`.
</Card>
</Cards> </Cards>
In case we don't have a client SDK for your language yet, you can use the C SDK. It's written in 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"; import { FileCodeCorner } from "lucide-react";
<Card <Card title="Write Your Own SDK" icon={<FileCodeCorner />} href="/guides/write-your-own-sdk">
title="Write Your Own SDK" Learn how to write your own SDK in any language.
icon={<FileCodeCorner />}
href="/guides/write-your-own-sdk"
>
Learn how to write your own SDK in any language.
</Card> </Card>
-4
View File
@@ -1,4 +0,0 @@
---
title: C
icon: SiC
---
-4
View File
@@ -1,4 +0,0 @@
---
title: Go
icon: SiGo
---
+120 -66
View File
@@ -1,20 +1,20 @@
--- ---
title: JavaScript (Client-Side) title: JavaScript
description: Learn how to integrate Duckity into your web application. description: Learn how to integrate Duckity into your web application.
icon: SiJavascript icon: SiJavascript
--- ---
Welcome to the Duckity JavaScript + WASM SDK documentation! This guide will teach you how to Welcome to the Duckity JavaScript SDK documentation! This guide will teach you how to install and
install and set up the SDK in no time. set up the SDK in no time.
The following SDKs depend on this one and provide specialized wrappers for different frameworks: The following SDKs depend on this one and provide specialized wrappers for different frameworks:
import { SiReact } from "@icons-pack/react-simple-icons"; import { SiReact } from "@icons-pack/react-simple-icons";
<Cards> <Cards>
<Card href="/sdks/react" title="React" icon={<SiReact />}> <Card href="/sdks/react" title="React" icon={<SiReact />}>
Integrate client-side code with Duckity using the React SDK. Integrate client-side code with Duckity using the React SDK.
</Card> </Card>
</Cards> </Cards>
## Quick Start ## 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: Follow these steps depending on your application:
<Tabs items={["Installed", "Via CDN"]}> <Tabs items={["Installed", "Via CDN"]}>
<Tab> <Tab>
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 ```package-install
@duckity/js @duckity/js
``` ```
Then import it in your code: Then import it in your code:
```ts ```ts
import duckity from "@duckity/js"; import * as duckity from "@duckity/js";
``` ```
</Tab>
<Tab>
If you're using the SDK from a static site, import it using a CDN like
[esm.sh](https://esm.sh/) instead.
</Tab> ```html
<Tab> <script type="module">
If you're using the SDK from a static site, import it using a CDN like [esm.sh](https://esm.sh/) // Using esm.sh
instead. import * as duckity from "https://esm.sh/@duckity/js";
```html // Using jsdelivr.net
<script type="module"> import * as duckity from "https://cdn.jsdelivr.net/npm/@duckity/js";
// Using esm.sh
import duckity from "https://esm.sh/@duckity/js";
// Using jsdelivr.net // Using UNPKG
import duckity from "https://cdn.jsdelivr.net/npm/@duckity/js"; import * as duckity from "https://unpkg.com/@duckity/js";
</script>
```
</Tab>
// Using UNPKG
import duckity from "https://unpkg.com/@duckity/js";
</script>
```
</Tab>
</Tabs> </Tabs>
### Solve a Challenge [step] ### 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()`. `duckity.solve()`.
```ts ```ts
const PROTECTION_PROFILE_ID: string = "";
// ---cut---
import duckity from "@duckity/js"; 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);
``` ```
<Callout title="TIP"> ### Validate a Challenge [step]
Hover over the code to see the type definitions.
</Callout> 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 ## Advanced Usage
Solving a challenge on request works well for simple setups. However, both security and UX can be Solving a challenge on request works well for simple setups. However, UX can be greatly improved
greatly improved changing a few settings and planning when to solve challenges. 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 ### Asynchronous Challenge Solving
settings), issue the challenge as soon as possible. Note, however, that the challenge
### 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 For example, if the user opens a login page, you can infer the user will need a challenge solved to
`duckity.solve()`. be able to attempt to log in. You can solve a challenge beforehand so that the user does not need to
wait.
```ts ```html
const PROTECTION_PROFILE_ID: string = "py83YHkXV6ZpIsJZGVxzS"; <!doctype html>
// ---cut--- <html>
import duckity from "@duckity/js"; <head>
<style>
.hidden {
display: none;
}
</style>
</head>
<body>
<form id="form">
<input name="email" />
<input name="password" />
<button type="submit" id="submit-button">Log In</button>
<div id="submit-loading" class="hidden">Logging you in...</div>
</form>
<script type="module">
import * as duckity from "https://esm.sh/@duckity/js";
let solution = await duckity.solve(PROTECTION_PROFILE_ID); const PROTECTION_PROFILE_ID = "<your-protection-profile-id>";
const form = document.getElementById("form");
const submitButton = document.getElementById("submit-button");
const submitLoading = document.getElementById("submit-loading");
// A Promise<string>, it will solve a challenge in the background while the user logs
// in. Do not `await` it here, it will prevent the `form.addEventListener()` call from
// running until the challenge is solved, which takes some time.
let solutionPromise = duckity.solve(PROTECTION_PROFILE_ID);
form.addEventListener("submit", async (e) => {
e.preventDefault();
submitButton.classList.add("hidden");
submitLoading.classList.remove("hidden");
let solution = await solutionPromise;
// Do your stuff.
});
</script>
</body>
</html>
``` ```
### Using On Self-Hosted Ducklings <Callout>
Solution tokens expire after the time set in the dashboard. In case of failure to validate, try
refreshing the challenge (calling `duckity.solve()` again).
</Callout>
Self-hosted ducklings are hosted at a different domain from Duckity-hosted ducklings. To point it ## Contributing & License
to a custom domain, change the following setting:
```ts All contributions are welcome to the SDK. Whether it's bug fixes, suggestions, new features,
const PROTECTION_PROFILE_ID: string = "py83YHkXV6ZpIsJZGVxzS"; documentation updates, or fixing a typo, if you think you can make this SDK better, feel free to
// ---cut--- make a pull request in the [GitHub repository](https://github.com/duckity-com/sdks).
import duckity from "@duckity/js";
let solution = await duckity.solve( This SDK is licensed under the permissive MIT License, and so will be all contributions to the SDK.
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.
+448 -1
View File
@@ -1,4 +1,451 @@
--- ---
title: Python 3 title: Python 3
icon: SiPython icon: SiPython
--- ---
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.
<Callout>
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.
</Callout>
## 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 */}
<Tabs items={["pip", "uv", "poetry"]} groupId="python-package-manager" persist>
<Tab value="pip" id="pip">
```sh
pip install duckity
```
</Tab>
<Tab value="uv" id="uv">
```sh
uv add duckity
```
</Tab>
<Tab value="poetry" id="poetry">
```sh
poetry add duckity
```
</Tab>
</Tabs>
### Solve a Challenge [step]
Solving a challenge only requires a protection profile ID.
<Tabs items={["Async", "Sync"]} groupId="python-sync" persist>
<Tab value="Async" id="async">
```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.
</Tab>
<Tab value="Sync" id="sync">
```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.
</Tab>
</Tabs>
### 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:
<Tabs items={["Async", "Sync"]} groupId="python-sync" persist>
<Tab value="Async" id="async">
```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
)
```
</Tab>
<Tab value="Sync" id="sync">
```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)
)
```
</Tab>
</Tabs>
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.
+207 -1
View File
@@ -1,4 +1,210 @@
--- ---
title: React title: React
icon: SiReact icon: SiReact
--- ---
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";
<Cards>
<Card href="/sdks/javascript" title="JavaScript" icon={<SiJavascript />}>
Integrate client and server-side code with Duckity using the JavaScript/TypeScript SDK.
</Card>
</Cards>
## 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:
<Tabs items={["Next.js", "Vite", "Remix", "Gatsby", "Expo Web", "Astro"]} groupId="react-framework" persist>
<Tab value="Next.js" id="nextjs">
```ts
"use client";
import { useChallenge } from "@duckity/react";
function MyComponent() {
const duckity = useChallenge(process.env.NEXT_PUBLIC_DUCKITY_PROTECTION_PROFILE_ID);
}
```
</Tab>
<Tab value="Vite" id="vite">
```ts
import { useChallenge } from "@duckity/react";
function MyComponent() {
const duckity = useChallenge(import.meta.env.VITE_DUCKITY_PROTECTION_PROFILE_ID);
}
```
</Tab>
<Tab value="Remix" id="remix">
```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<typeof loader>();
const duckity = useChallenge(data.ENV.DUCKITY_PROTECTION_PROFILE_ID);
}
```
</Tab>
<Tab value="Gatsby" id="gatsby">
```ts
import { useChallenge } from "@duckity/react";
function MyComponent() {
const duckity = useChallenge(process.env.GATSBY_DUCKITY_PROTECTION_PROFILE_ID);
}
```
</Tab>
<Tab value="Expo Web" id="expo-web">
```ts
import { useChallenge } from "@duckity/react";
function MyComponent() {
const duckity = useChallenge(process.env.EXPO_PUBLIC_DUCKITY_PROTECTION_PROFILE_ID);
}
```
</Tab>
<Tab value="Astro" id="astro">
```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
<MyComponent client:load />
```
</Tab>
</Tabs>
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<string>`.
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<HTMLFormElement>) {
e.preventDefault();
const solution: string = await duckity.wait();
// Submit the form to your backend.
}
return (
<form onSubmit={handleSubmit}>
<input required placeholder="Email" type="email" />
<input required placeholder="Password" type="Password" />
<button type="submit">Log In</button>
</form>
)
}
```
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()`.
<TypeTable
type={{
refresh: {
description:
"Discards the current challenge solution and solves a new challenge. Note that if the challenge is currently being fetched or solved, calling this function will not stop the ongoing process and the solution still will be set once that solving the challenge completes.",
type: "function",
returns: "void",
required: true,
},
wait: {
description: "Waits until a challenge solution is available and returns it.",
type: "function",
returns: "Promise<string>",
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.
-87
View File
@@ -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": "<challenge-string>"
}
```
### 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("<your-challenge-token>");
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).
-90
View File
@@ -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).
<Callout>
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.
</Callout>
### 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 = "<your-protection-profile-id>";
#[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.
+364
View File
@@ -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.
<Callout>
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.
</Callout>
### 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 = "<your-own-protection-profile-id>";
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<Challenge, duckity::core::DuckityDecodeError>`
2. `duckity::core::solve(&Challenge) -> Solution`
3. `duckity::core::encode(&str, &Solution) -> Result<String, duckity::core::DuckityEncodeError>`
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 = "<insert-your-protection-profile-here>";
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 = "<your-application-secret>";
const PROTECTION_PROFILE_ID: &str = "<your-protection-profile-id>";
#[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::<SocketAddr>(),
)
.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<SocketAddr>,
Json(payload): Json<ProtectedRequestPayload>,
) -> 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 = "<your-application-secret>";
const PROTECTION_PROFILE_ID: &str = "<your-protection-profile-id>";
#[tokio::main]
async fn main() {
let protected = warp::path("protected")
.and(warp::post())
.and(warp::addr::remote())
.and(warp::body::json::<ProtectedRequestPayload>())
.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<SocketAddr>,
payload: ProtectedRequestPayload,
) -> Result<impl Reply, Infallible> {
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 = "<your-application-secret>";
const PROTECTION_PROFILE_ID: &str = "<your-protection-profile-id>";
#[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<ProtectedRequestPayload>) -> 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.
+1
View File
@@ -21,6 +21,7 @@
"@typescript/vfs": "^1.6.4", "@typescript/vfs": "^1.6.4",
"beautiful-mermaid": "^1.1.3", "beautiful-mermaid": "^1.1.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"cn": "^0.2.4",
"cnfast": "^0.0.8", "cnfast": "^0.0.8",
"fumadocs-core": "16.12.1", "fumadocs-core": "16.12.1",
"fumadocs-mdx": "15.2.0", "fumadocs-mdx": "15.2.0",
+2
View File
@@ -4,6 +4,7 @@ import Image from "next/image";
import { Mermaid } from '@/components/mermaid'; import { Mermaid } from '@/components/mermaid';
import type { MDXComponents } from "mdx/types"; import type { MDXComponents } from "mdx/types";
import * as Twoslash from 'fumadocs-twoslash/ui'; import * as Twoslash from 'fumadocs-twoslash/ui';
import { TypeTable } from 'fumadocs-ui/components/type-table';
export function getMDXComponents(components?: MDXComponents) { export function getMDXComponents(components?: MDXComponents) {
return { return {
@@ -12,6 +13,7 @@ export function getMDXComponents(components?: MDXComponents) {
...TabsComponents, ...TabsComponents,
...Twoslash, ...Twoslash,
Mermaid, Mermaid,
TypeTable,
...components, ...components,
} satisfies MDXComponents; } satisfies MDXComponents;
} }
+183
View File
@@ -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<string, TypeNode> } & ComponentProps<'div'>) {
const t = useTranslations({ note: 'type table' });
return (
<div
id={id}
className={cn(
'@container flex flex-col p-1 bg-fd-card text-fd-card-foreground rounded-2xl border my-6 text-sm overflow-hidden',
className,
)}
{...props}
>
<div className="flex font-medium items-center px-3 py-1 not-prose text-fd-muted-foreground">
<p className="w-1/4">{t('Prop')}</p>
<p className="@max-xl:hidden">{t('Type')}</p>
</div>
{Object.entries(type).map(([key, value]) => (
<Item key={key} parentId={id} name={key} item={value} />
))}
</div>
);
}
function Item({
parentId,
name,
item: {
parameters = [],
description,
required = false,
deprecated,
typeDescription,
default: defaultValue,
type,
typeDescriptionLink,
returns,
},
}: {
parentId?: string;
name: string;
item: TypeNode;
}) {
const t = useTranslations({ note: 'type table' });
const [open, setOpen] = useState(false);
const id = parentId ? `${parentId}-${name}` : undefined;
useEffect(() => {
const hash = window.location.hash;
if (!id || !hash) return;
if (`#${id}` === hash) setOpen(true);
}, [id]);
return (
<Collapsible
id={id}
open={open}
onOpenChange={(v) => {
if (v && id) {
window.history.replaceState(null, '', `#${id}`);
}
setOpen(v);
}}
className={cn(
'rounded-xl border overflow-hidden scroll-m-20 transition-all',
open ? 'shadow-sm bg-fd-background not-last:mb-2' : 'border-transparent',
)}
>
<CollapsibleTrigger className="relative flex flex-row items-center w-full group text-start px-3 py-2 not-prose hover:bg-fd-accent">
<code
className={cn(
'text-fd-primary min-w-fit w-1/4 font-mono font-medium pe-2',
deprecated && 'line-through text-fd-primary/50',
)}
>
{name}
{!required && '?'}
</code>
{typeDescriptionLink ? (
<Link href={typeDescriptionLink} className="underline @max-xl:hidden">
{type}
</Link>
) : (
<span className="@max-xl:hidden">{type}</span>
)}
<ChevronDown className="absolute inset-e-2 size-4 text-fd-muted-foreground transition-transform group-data-[open]:rotate-180" />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="grid grid-cols-[1fr_3fr] gap-y-4 text-sm p-3 overflow-auto fd-scroll-container border-t">
<div className="text-sm prose col-span-full prose-no-margin empty:hidden">
{description}
</div>
{typeDescription && (
<>
<p className={cn(fieldVariants())}>{t('Type')}</p>
<p className="my-auto not-prose">{typeDescription}</p>
</>
)}
{defaultValue && (
<>
<p className={cn(fieldVariants())}>{t('Default')}</p>
<p className="my-auto not-prose">{defaultValue}</p>
</>
)}
{parameters.length > 0 && (
<>
<p className={cn(fieldVariants())}>{t('Parameters')}</p>
<div className="flex flex-col gap-2">
{parameters.map((param) => (
<div key={param.name} className="inline-flex items-center flex-wrap gap-1">
<p className="font-medium not-prose text-nowrap">{param.name} -</p>
<div className="text-sm prose prose-no-margin">{param.description}</div>
</div>
))}
</div>
</>
)}
{returns && (
<>
<p className={cn(fieldVariants())}>{t('Returns')}</p>
<div className="my-auto text-sm prose prose-no-margin">{returns}</div>
</>
)}
</div>
</CollapsibleContent>
</Collapsible>
);
}
+32
View File
@@ -0,0 +1,32 @@
'use client';
import { Collapsible as Primitive } from '@base-ui/react/collapsible';
import type { ComponentProps } from 'react';
import { cn } from '../../lib/cn';
export const Collapsible = Primitive.Root;
export const CollapsibleTrigger = Primitive.Trigger;
export function CollapsibleContent({
children,
className,
...props
}: ComponentProps<typeof Primitive.Panel>) {
return (
<Primitive.Panel
{...props}
className={(s) =>
cn(
"overflow-hidden [&[hidden]:not([hidden='until-found'])]:hidden h-(--collapsible-panel-height) transition-[height,opacity] data-starting-style:opacity-0 data-starting-style:h-0 data-ending-style:h-0 data-ending-style:opacity-0",
typeof className === 'function' ? className(s) : className,
)
}
>
{children}
</Primitive.Panel>
);
}
export type CollapsibleProps = Primitive.Root.Props;
export type CollapsibleContentProps = Primitive.Panel.Props;
export type CollapsibleTriggerProps = Primitive.Trigger.Props;