arcjet / arcjet-js

Arcjet JS SDKs. Rate limiting, bot protection, email verification & attack defense for Node.js, Next.js, Bun & SvelteKit.
https://arcjet.com
Apache License 2.0
234 stars 5 forks source link

Support for Remix #1313

Open justinfarrelldev opened 1 month ago

justinfarrelldev commented 1 month ago

Hello, I see that this project has Next.js support, however it is unclear whether Remix support is in the works or works out-of-the-box for the Next.js package. Are there perhaps any plans to include Remix (or, I guess since it is being merged with React Router, React Router) support?

Thank you, and I love this project so far! I can tell this will be extremely useful when I develop APIs in the future.

davidmytton commented 1 month ago

Hi @justinfarrelldev! We don't have official Remix support yet, but Arcjet will work and we know of several people using it without any issues. However, support depends on which Remix adapter you're using - Arcjet will work with the default Remix server (which uses Express) and also with Node or Vercel.

I just tested using our Node SDK in a Remix loader function and it worked correctly. There is a type-mismatch warning when you pass the request parameter into aj.project() but you can ignore it.

This uses https://github.com/sergiodxa/remix-utils#getclientipaddress to get the IP address, which is installed with:

npm install remix-utils is-ip

Then you can manually construct the request props with the IP included:

import arcjet, { tokenBucket } from "arcjet";
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { getClientIPAddress } from "remix-utils/get-client-ip-address";

const aj = arcjet({
  key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
  characteristics: ["userId"], // track requests by a custom user ID
  rules: [
    // Create a token bucket rate limit. Other algorithms are supported.
    tokenBucket({
      mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
      refillRate: 5, // refill 5 tokens per interval
      interval: 10, // refill every 10 seconds
      capacity: 10, // bucket maximum capacity of 10 tokens
    }),
  ],
});

export async function loader({
  request,
}: LoaderFunctionArgs) {
  // Construct an object with Arcjet request details
  const path = new URL(request.url || "", `http://${request.headers.get("host")}`);
  const details = {
    ip: getClientIPAddress(request),
    method: request.method,
    host: request.headers.get("host"),
    url: path.pathname,
    headers: request.headers,
  };

  const userId = "user123"; // Replace with your authenticated user ID
  const decision = await aj.protect(details, { userId, requested: 5 }); // Deduct 5 tokens from the bucket
  console.log("Arcjet decision", decision);

  if (decision.isDenied()) {
    throw new Response("Too many requests", {
      status: 429,
    });
  } else {
    return null;
  }
}

...

If you create a custom express server for Remix, then you can also put Arcjet in there e.g. for running Shield on every request. We have an example with a generic Express server at https://github.com/arcjet/arcjet-js/blob/main/examples/nodejs-express-rl/index.js

justinfarrelldev commented 1 month ago

Hi @justinfarrelldev! We don't have official Remix support yet, but Arcjet will work and we know of several people using it without any issues. However, support depends on which Remix adapter you're using - Arcjet will work with the default Remix server (which uses Express) and also with Node or Vercel.

I just tested using our Node SDK in a Remix loader function and it worked correctly. There is a type-mismatch warning when you pass the request parameter into aj.project() but you can ignore it.

import arcjet, { tokenBucket } from "@arcjet/node";
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";

const aj = arcjet({
  key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
  characteristics: ["userId"], // track requests by a custom user ID
  rules: [
    // Create a token bucket rate limit. Other algorithms are supported.
    tokenBucket({
      mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
      refillRate: 5, // refill 5 tokens per interval
      interval: 10, // refill every 10 seconds
      capacity: 10, // bucket maximum capacity of 10 tokens
    }),
  ],
});

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const userId = "user123"; // Replace with your authenticated user ID
  const decision = await aj.protect(request, { userId, requested: 5 }); // Deduct 5 tokens from the bucket
  console.log("Arcjet decision", decision);

  if (decision.isDenied()) {
    throw new Response("Too many requests", {
      status: 429,
    });
  } else {
    return null;
  }
}

...

If you create a custom express server for Remix, then you can also put Arcjet in there e.g. for running Shield on every request. We have an example with a generic Express server at https://github.com/arcjet/arcjet-js/blob/main/examples/nodejs-express-rl/index.js

Awesome, thank you so much for the extremely thorough reply! I'll definitely give it a go 😁

davidmytton commented 1 month ago

On testing this in production, Remix doesn't actually provide the IP - when you use Arcjet locally it's using a local IP.

From the discussion at https://github.com/remix-run/remix/discussions/2413 the suggestion is to use a utility from https://github.com/sergiodxa/remix-utils#getclientipaddress to get the IP address. We'll look at how we can do this for you in our official SDK, but for now I've adjusted the above code sample (https://github.com/arcjet/arcjet-js/issues/1313#issuecomment-2278377980) to use it.