Checkpoint
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.tsx",
|
||||
"*.jsx",
|
||||
"*.ts",
|
||||
"*.js"
|
||||
],
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.mdx",
|
||||
"*.md"
|
||||
],
|
||||
"options": {
|
||||
"tabWidth": 4,
|
||||
"proseWrap": "always",
|
||||
"printWidth": 100
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": ["[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=="],
|
||||
|
||||
"collapse-white-space": ["[email protected]", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
|
||||
|
||||
@@ -711,7 +711,7 @@ In TypeScript, this equals to
|
||||
|
||||
</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
|
||||
different bases in `is_prime()`, we increase the likelyhood of a number being prime. The test works
|
||||
|
||||
+1
-4
@@ -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"
|
||||
]
|
||||
}
|
||||
+25
-45
@@ -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.
|
||||
|
||||
<Callout>
|
||||
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.
|
||||
</Callout>
|
||||
|
||||
## 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.
|
||||
|
||||
<Callout>
|
||||
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.
|
||||
</Callout>
|
||||
|
||||
## 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";
|
||||
|
||||
<Cards>
|
||||
<Card href="/sdks/javascript" title="JavaScript" icon={<SiJavascript />}>
|
||||
Integrate client-side code with Duckity using the JavaScript SDK.
|
||||
</Card>
|
||||
<Card href="/sdks/react" title="React" icon={<SiReact />}>
|
||||
Integrate client-side code with Duckity using the React SDK.
|
||||
</Card>
|
||||
<Card href="/sdks/c" title="C" icon={<SiC />}>
|
||||
Integrate client-side code with Duckity using the C SDK.
|
||||
</Card>
|
||||
<Card href="/sdks/python" title="Python 3" icon={<SiPython />}>
|
||||
Integrate client-side code with Duckity using the Python SDK.
|
||||
</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>
|
||||
<Card href="/sdks/javascript" title="JavaScript" icon={<SiJavascript />}>
|
||||
Integrate Duckity into your application using the JavaScript SDK.
|
||||
</Card>
|
||||
<Card href="/sdks/react" title="React" icon={<SiReact />}>
|
||||
Integrate Duckity into your React app using the React SDK.
|
||||
</Card>
|
||||
<Card href="/sdks/python" title="Python 3" icon={<SiPython />}>
|
||||
Integrate Duckity into your application using the Python SDK.
|
||||
</Card>
|
||||
<Card href="/sdks/rust" title="Rust" icon={<SiRust />}>
|
||||
Integrate Duckity into your application using the Rust SDK. Supports no-std environments.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
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";
|
||||
|
||||
<Card
|
||||
title="Write Your Own SDK"
|
||||
icon={<FileCodeCorner />}
|
||||
href="/guides/write-your-own-sdk"
|
||||
>
|
||||
Learn how to write your own SDK in any language.
|
||||
<Card title="Write Your Own SDK" icon={<FileCodeCorner />} href="/guides/write-your-own-sdk">
|
||||
Learn how to write your own SDK in any language.
|
||||
</Card>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
title: C
|
||||
icon: SiC
|
||||
---
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
title: Go
|
||||
icon: SiGo
|
||||
---
|
||||
+120
-66
@@ -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";
|
||||
|
||||
<Cards>
|
||||
<Card href="/sdks/react" title="React" icon={<SiReact />}>
|
||||
Integrate client-side code with Duckity using the React SDK.
|
||||
</Card>
|
||||
<Card href="/sdks/react" title="React" icon={<SiReact />}>
|
||||
Integrate client-side code with Duckity using the React SDK.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## 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:
|
||||
|
||||
<Tabs items={["Installed", "Via CDN"]}>
|
||||
<Tab>
|
||||
Run the following line in your terminal to install the Duckity SDK.
|
||||
<Tab>
|
||||
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";
|
||||
```
|
||||
</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>
|
||||
<Tab>
|
||||
If you're using the SDK from a static site, import it using a CDN like [esm.sh](https://esm.sh/)
|
||||
instead.
|
||||
```html
|
||||
<script type="module">
|
||||
// Using esm.sh
|
||||
import * as duckity from "https://esm.sh/@duckity/js";
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
// Using esm.sh
|
||||
import duckity from "https://esm.sh/@duckity/js";
|
||||
// Using jsdelivr.net
|
||||
import * as duckity from "https://cdn.jsdelivr.net/npm/@duckity/js";
|
||||
|
||||
// Using jsdelivr.net
|
||||
import duckity from "https://cdn.jsdelivr.net/npm/@duckity/js";
|
||||
// Using UNPKG
|
||||
import * as duckity from "https://unpkg.com/@duckity/js";
|
||||
</script>
|
||||
```
|
||||
</Tab>
|
||||
|
||||
// Using UNPKG
|
||||
import duckity from "https://unpkg.com/@duckity/js";
|
||||
</script>
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 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);
|
||||
```
|
||||
|
||||
<Callout title="TIP">
|
||||
Hover over the code to see the type definitions.
|
||||
</Callout>
|
||||
### 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
|
||||
<!doctype html>
|
||||
<html>
|
||||
<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
|
||||
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.
|
||||
|
||||
@@ -2,3 +2,450 @@
|
||||
title: Python 3
|
||||
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.
|
||||
|
||||
@@ -2,3 +2,209 @@
|
||||
title: React
|
||||
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.
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user