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

{t('Prop')}

+

{t('Type')}

+
+ {Object.entries(type).map(([key, value]) => ( + + ))} +
+ ); +} + +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 ( + { + 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', + )} + > + + + {name} + {!required && '?'} + + {typeDescriptionLink ? ( + + {type} + + ) : ( + {type} + )} + + + +
+
+ {description} +
+ {typeDescription && ( + <> +

{t('Type')}

+

{typeDescription}

+ + )} + {defaultValue && ( + <> +

{t('Default')}

+

{defaultValue}

+ + )} + {parameters.length > 0 && ( + <> +

{t('Parameters')}

+
+ {parameters.map((param) => ( +
+

{param.name} -

+
{param.description}
+
+ ))} +
+ + )} + {returns && ( + <> +

{t('Returns')}

+
{returns}
+ + )} +
+
+
+ ); +} diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..7d7b261 --- /dev/null +++ b/src/components/ui/collapsible.tsx @@ -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) { + return ( + + 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} + + ); +} + +export type CollapsibleProps = Primitive.Root.Props; +export type CollapsibleContentProps = Primitive.Panel.Props; +export type CollapsibleTriggerProps = Primitive.Trigger.Props;