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
250 stars 5 forks source link

Support for Astro #1075

Open davidmytton opened 3 months ago

davidmytton commented 3 months ago

Astro allows middleware and provides the request information in the context. Per the docs, this is a standard Request object.

If we try to pass this through to aj.protect there is a type error:

Argument of type 'Request' is not assignable to parameter of type 'ArcjetNodeRequest'.
  Types of property 'headers' are incompatible.
    Type 'Headers' is not assignable to type 'Record<string, string | string[] | undefined>'.
      Index signature for type 'string' is missing in type 'Headers'.ts(2345)
(property) AstroSharedContext<Record<string, any>, Params>.request: Request

Note that Astro supports several rendering modes, including static (where middleware doesn't make sense) as well as multiple runtime adapters. So we'll need to make it clear that we require server mode otherwise you get this warning:

15:09:15 [WARN] `Astro.request.headers` is unavailable in "static" output mode, and in prerendered pages 
within "hybrid" and "server" output modes. If you need access to request headers, make sure that `output` 
is configured as either `"server"` or `output: "hybrid"` in your config file, and that the page accessing the 
headers is rendered on-demand.
blaine-arcjet commented 3 months ago

ArcjetNodeRequest are you trying to use @arcjet/node here? That's not going to be the right adapter as node doesn't work with Request

blaine-arcjet commented 3 months ago

Astro supports several rendering modes, including static

The unfortunate thing about this is that it seems like Astro handles rendering modes transparently and provides a request object but only makes request.url available in static mode. This won't work for our SDK.

davidmytton commented 3 months ago

ArcjetNodeRequest are you trying to use @arcjet/node here? That's not going to be the right adapter as node doesn't work with Request

Yes. It didn't seem right to use one of the other adapters, but using @arcjet/next works so long as you have server rendering mode enabled.

astro.config.mjs

import { defineConfig } from 'astro/config';

import vercel from "@astrojs/vercel/serverless";

// https://astro.build/config
export default defineConfig({
  output: "server",
  adapter: vercel()
});

src/middleware.ts

import { defineMiddleware } from "astro:middleware";
import arcjet, { shield } from "@arcjet/next";

const aj = arcjet({
    // Get your site key from https://app.arcjet.com
    // and set it as an environment variable rather than hard coding.
    key: "ajkey_01hm7kbgdaexeambd6w9fdpy8x",
    rules: [
      // Protect against common attacks with Arcjet Shield
      shield({
        mode: "DRY_RUN", // Change to "LIVE" to block requests
      }),
    ],
  });

// `context` and `next` are automatically typed
export const onRequest = defineMiddleware(async (context, next) => {
    const decision = await aj.protect(context.request);
    console.log(decision)

    return next();
});

Result:

 astro  v4.11.4 ready in 82 ms

┃ Local    http://localhost:4321/
┃ Network  use --host to expose

15:18:47 watching for file changes...
15:18:48 [200] / 18ms
15:18:52 [watch] src/middleware.ts
✦Aj WARN Using 127.0.0.1 as IP address in development mode
ArcjetAllowDecision {
  id: 'req_01j1wfcsycfmk8e3jct37rynnm',
  ttl: 0,
  results: [
    ArcjetRuleResult {
      ruleId: '',
      ttl: 0,
      state: 'RUN',
      conclusion: 'ALLOW',
      reason: [ArcjetShieldReason]
    }
  ],
  ip: ArcjetIpDetails {
    latitude: undefined,
    longitude: undefined,
    accuracyRadius: undefined,
    timezone: undefined,
    postalCode: undefined,
    city: undefined,
    region: undefined,
    country: undefined,
    countryName: undefined,
    continent: undefined,
    continentName: undefined,
    asn: undefined,
    asnName: undefined,
    asnDomain: undefined,
    asnType: undefined,
    asnCountry: undefined,
    service: undefined
  },
  conclusion: 'ALLOW',
  reason: ArcjetShieldReason { type: 'SHIELD', shieldTriggered: false }
}
15:18:52 [200] / 249ms
blaine-arcjet commented 3 months ago

Yeah, I figured that next or another Request adapter would "work". However, I don't think it will work in production mode because there's no IP available in the Astro request. We'll need to investigate Astro.clientAddress to get an IP

davidmytton commented 3 months ago

Right. This is also working with the Vercel adapter locally, but there are others like Cloudflare and Deno Deploy that will probably need #759 and #758.

blaine-arcjet commented 3 months ago

There are others like Cloudflare and Deno Deploy that will probably need https://github.com/arcjet/arcjet-js/issues/759 and https://github.com/arcjet/arcjet-js/issues/758.

I hope not. This should just need an Astro adapter. The @arcjet/next package !== vercel adapter for astro. You are just getting it to work because Request is fairly standardized.