{"id":13564706,"url":"https://github.com/arcjet/arcjet-js","last_synced_at":"2026-04-14T21:01:55.914Z","repository":{"id":221754917,"uuid":"725458907","full_name":"arcjet/arcjet-js","owner":"arcjet","description":"Arcjet JavaScript (JS) / TypeScript SDK. Stop bots and automated attacks from burning your AI budget, leaking data, or misusing tools with Arcjet's AI security building blocks.","archived":false,"fork":false,"pushed_at":"2026-04-14T18:11:21.000Z","size":60029,"stargazers_count":659,"open_issues_count":83,"forks_count":29,"subscribers_count":8,"default_branch":"main","last_synced_at":"2026-04-14T18:14:09.425Z","etag":null,"topics":["bun","developer-tools","javascript","javascript-library","nextjs","nodejs","prompt-injection","prompt-injection-defense","rate-limiting","remix","security","security-tools","serverless","sveltekit","typescript","typescript-library","webassembly"],"latest_commit_sha":null,"homepage":"https://arcjet.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arcjet.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-11-30T07:25:40.000Z","updated_at":"2026-04-14T16:57:14.000Z","dependencies_parsed_at":"2024-06-10T08:14:27.008Z","dependency_job_id":"4e9bf0c2-1a1b-4394-8166-36a14e22e029","html_url":"https://github.com/arcjet/arcjet-js","commit_stats":{"total_commits":1304,"total_committers":8,"mean_commits":163.0,"dds":"0.22162576687116564","last_synced_commit":"7d9ac4f6401c2e47632c8dc97845f6cd3abf92f9"},"previous_names":["arcjet/arcjet-js"],"tags_count":59,"template":false,"template_full_name":null,"purl":"pkg:github/arcjet/arcjet-js","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcjet%2Farcjet-js","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcjet%2Farcjet-js/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcjet%2Farcjet-js/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcjet%2Farcjet-js/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arcjet","download_url":"https://codeload.github.com/arcjet/arcjet-js/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcjet%2Farcjet-js/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31815080,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-14T18:05:02.291Z","status":"ssl_error","status_checked_at":"2026-04-14T18:05:01.765Z","response_time":153,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bun","developer-tools","javascript","javascript-library","nextjs","nodejs","prompt-injection","prompt-injection-defense","rate-limiting","remix","security","security-tools","serverless","sveltekit","typescript","typescript-library","webassembly"],"created_at":"2024-08-01T13:01:34.715Z","updated_at":"2026-04-14T21:01:55.908Z","avatar_url":"https://github.com/arcjet.png","language":"TypeScript","funding_links":[],"categories":["Web Frontend","TypeScript","webassembly","🌐 Web Development - Frontend"],"sub_categories":["Next.js"],"readme":"\u003ca href=\"https://arcjet.com\" target=\"_arcjet-home\"\u003e\n  \u003cpicture\u003e\n    \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://arcjet.com/logo/arcjet-dark-lockup-voyage-horizontal.svg\"\u003e\n    \u003cimg src=\"https://arcjet.com/logo/arcjet-light-lockup-voyage-horizontal.svg\" alt=\"Arcjet Logo\" height=\"128\" width=\"auto\"\u003e\n  \u003c/picture\u003e\n\u003c/a\u003e\n\n# Arcjet - JS SDK\n\n\u003cp\u003e\n  \u003cpicture\u003e\n    \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.shields.io/npm/v/arcjet?style=flat-square\u0026label=%E2%9C%A6Aj\u0026labelColor=000000\u0026color=5C5866\"\u003e\n    \u003cimg alt=\"npm badge\" src=\"https://img.shields.io/npm/v/arcjet?style=flat-square\u0026label=%E2%9C%A6Aj\u0026labelColor=ECE6F0\u0026color=ECE6F0\"\u003e\n  \u003c/picture\u003e\n\u003c/p\u003e\n\n[Arcjet][arcjet] is the runtime security platform that ships with your AI code. Stop bots and automated attacks from burning your AI budget, leaking data, or misusing tools with Arcjet's AI security building blocks.\n\nThis is the monorepo containing various [Arcjet][arcjet] open source packages\nfor JS.\n\n## Getting started\n\n1. **Get your API key** — [Sign up at `app.arcjet.com`](https://app.arcjet.com).\n2. **Install the SDK for your framework:** Every feature works with any JavaScript application.\n\n| Framework    | Package                         | Install                      |\n| ------------ | ------------------------------- | ---------------------------- |\n| Next.js      | `@arcjet/next`                  | `npm i @arcjet/next`         |\n| Node.js      | `@arcjet/node`                  | `npm i @arcjet/node`         |\n| Bun          | `@arcjet/bun`                   | `bun add @arcjet/bun`        |\n| Deno         | `@arcjet/deno`                  | `deno add npm:@arcjet/deno`  |\n| Express      | `@arcjet/node`                  | `npm i @arcjet/node`         |\n| Fastify      | `@arcjet/fastify`               | `npm i @arcjet/fastify`      |\n| Hono         | `@arcjet/node` or `@arcjet/bun` | `npm i @arcjet/node`         |\n| NestJS       | `@arcjet/nest`                  | `npm i @arcjet/nest`         |\n| Nuxt         | `@arcjet/nuxt`                  | `npm i @arcjet/nuxt`         |\n| Remix        | `@arcjet/remix`                 | `npm i @arcjet/remix`        |\n| React Router | `@arcjet/react-router`          | `npm i @arcjet/react-router` |\n| SvelteKit    | `@arcjet/sveltekit`             | `npm i @arcjet/sveltekit`    |\n| Astro        | `@arcjet/astro`                 | `npm i @arcjet/astro`        |\n\n3. **Set your environment variable:**\n\n```sh\n# .env.local (or your framework's env file)\nARCJET_KEY=ajkey_yourkey\n```\n\n4. **Protect a route** — see the [AI protection example](#vercel-ai-sdk-example)\n   or individual [feature examples](#features) below.\n\n### Get help\n\n[Join our Discord server][discord-invite] or [reach out for support][support].\n\n- [Documentation](https://docs.arcjet.com) — full reference and guides\n- [Example apps](#example-apps) — working starter projects for every framework\n- [Blueprints](#blueprints) — recipes for common security patterns\n\n## Features\n\n- 🔒 [Prompt Injection Detection](#prompt-injection-detection) — detect and block\n  prompt injection attacks before they reach your LLM.\n- 🤖 [Bot Protection](#bot-protection) — stop scrapers, credential stuffers, and\n  AI crawlers from abusing your endpoints.\n- 🛑 [Rate Limiting](#rate-limiting) — token bucket, fixed window, and sliding\n  window algorithms; model AI token budgets per user.\n- 🕵️ [Sensitive Information Detection](#sensitive-information-detection) — block\n  PII, credit cards, and custom patterns from entering your AI pipeline.\n- 🛡️ [Shield WAF](#shield-waf) — protect against SQL injection, XSS, and other\n  common web attacks.\n- 📧 [Email Validation](#email-validation) — block disposable, invalid, and\n  undeliverable addresses at signup.\n- 📝 [Signup Form Protection][feature-signup-protection] — combines bot\n  protection, email validation, and rate limiting to protect your signup forms.\n- 🎯 [Request Filters](#request-filters) — expression-based rules on IP, path,\n  headers, and custom fields.\n- 🌐 [IP Analysis](#ip-analysis) — geolocation, ASN, VPN, proxy, Tor, and hosting\n  detection included with every request.\n- 🧩 [Arcjet Guard](#arcjet-guard) — lower-level API for AI agent tool calls and\n  background tasks where there is no HTTP request.\n\n## Example apps\n\n- [Astro][github-arcjet-example-astro]\n- [Deno][github-arcjet-example-deno]\n- [Express][github-arcjet-example-express]\n- [FastAPI][github-arcjet-example-fastapi]\n- [Fastify][github-arcjet-example-fastify]\n- [NestJS][github-arcjet-example-nestjs]\n- [Next.js][github-arcjet-example-nextjs] ([try live][arcjet-example])\n- [Nuxt][github-arcjet-example-nuxt]\n- [React Router][github-arcjet-example-react-router]\n- [Remix][github-arcjet-example-remix]\n- [SvelteKit][github-arcjet-example-sveltekit]\n- [Tanstack Start][github-arcjet-example-tanstack-start]\n\n## Blueprints\n\n- [AI quota control][blueprint-ai-quota-control]\n- [Cookie banner][blueprint-cookie-banner]\n- [Custom rule][blueprint-custom-rule]\n- [IP geolocation][blueprint-ip-geolocation]\n- [Feedback form][blueprint-feedback-form]\n- [Malicious traffic][blueprint-malicious-traffic]\n- [Payment form][blueprint-payment-form]\n- [Sampling traffic][blueprint-sampling-traffic]\n- [VPN \u0026 proxy][blueprint-vpn-proxy]\n\n## Usage\n\nRead the docs at [`docs.arcjet.com`][arcjet-docs].\n\n\u003e **Note:** Examples below use `@arcjet/next` for illustration. Replace with\n\u003e the SDK for your runtime — `@arcjet/node`, `@arcjet/bun`, `@arcjet/sveltekit`,\n\u003e etc. The API is identical across all [SDKs](#sdks).\n\n### Vercel AI SDK example\n\nThis example protects a Next.js AI chat route using the [Vercel AI\nSDK][vercel-ai-sdk]: blocking automated clients that inflate costs, enforcing\nper-user token budgets, detecting sensitive information in messages, and\nblocking prompt injection attacks before they reach the model.\n\n```ts\n// app/api/chat/route.ts\nimport { openai } from \"@ai-sdk/openai\";\nimport arcjet, {\n  detectBot,\n  detectPromptInjection,\n  sensitiveInfo,\n  shield,\n  tokenBucket,\n} from \"@arcjet/next\";\nimport type { UIMessage } from \"ai\";\nimport { convertToModelMessages, isTextUIPart, streamText } from \"ai\";\n\nconst aj = arcjet({\n  key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com\n  // Track budgets per user — replace \"userId\" with any stable identifier\n  characteristics: [\"userId\"],\n  rules: [\n    // Shield protects against common web attacks e.g. SQL injection\n    shield({ mode: \"LIVE\" }),\n    // Block all automated clients — bots inflate AI costs\n    detectBot({\n      mode: \"LIVE\", // Blocks requests. Use \"DRY_RUN\" to log only\n      allow: [], // Block all bots. See https://arcjet.com/bot-list\n    }),\n    // Enforce budgets to control AI costs. Adjust rates and limits as needed.\n    tokenBucket({\n      mode: \"LIVE\",\n      refillRate: 2_000, // Refill 2,000 tokens per hour\n      interval: \"1h\",\n      capacity: 5_000, // Maximum 5,000 tokens in the bucket\n    }),\n    // Block messages containing sensitive information to prevent data leaks\n    sensitiveInfo({\n      mode: \"LIVE\",\n      // Block PII types that should never appear in AI prompts.\n      // Remove types your app legitimately handles (e.g. EMAIL for a support bot).\n      deny: [\"CREDIT_CARD_NUMBER\", \"EMAIL\"],\n    }),\n    // Detect prompt injection attacks before they reach your AI model\n    detectPromptInjection({\n      mode: \"LIVE\",\n    }),\n  ],\n});\n\nexport async function POST(req: Request) {\n  const userId = \"user-123\"; // Replace with your session/auth lookup\n  const { messages }: { messages: UIMessage[] } = await req.json();\n  const modelMessages = await convertToModelMessages(messages);\n\n  // Estimate token cost: ~1 token per 4 characters of text (rough heuristic)\n  const totalChars = modelMessages.reduce((sum, m) =\u003e {\n    const content =\n      typeof m.content === \"string\" ? m.content : JSON.stringify(m.content);\n    return sum + content.length;\n  }, 0);\n  const estimate = Math.ceil(totalChars / 4);\n\n  // Extract the most recent user message to scan for injection and PII\n  const lastMessage: string = (messages.at(-1)?.parts ?? [])\n    .filter(isTextUIPart)\n    .map((p) =\u003e p.text)\n    .join(\" \");\n\n  const decision = await aj.protect(req, {\n    userId,\n    requested: estimate,\n    sensitiveInfoValue: lastMessage,\n    detectPromptInjectionMessage: lastMessage,\n  });\n\n  if (decision.isDenied()) {\n    if (decision.reason.isBot()) {\n      return new Response(\"Automated clients are not permitted\", {\n        status: 403,\n      });\n    } else if (decision.reason.isRateLimit()) {\n      return new Response(\"AI usage limit exceeded\", { status: 429 });\n    } else if (decision.reason.isSensitiveInfo()) {\n      return new Response(\"Sensitive information detected\", { status: 400 });\n    } else if (decision.reason.isPromptInjection()) {\n      return new Response(\n        \"Prompt injection detected — please rephrase your message\",\n        { status: 400 },\n      );\n    } else {\n      return new Response(\"Forbidden\", { status: 403 });\n    }\n  }\n\n  const result = await streamText({\n    model: openai(\"gpt-4o\"),\n    messages: modelMessages,\n  });\n\n  return result.toUIMessageStreamResponse();\n}\n```\n\n### Prompt injection detection\n\nDetect and block prompt injection attacks — attempts to override your AI\nmodel's instructions — before they reach your model. Pass the user's message\nvia `detectPromptInjectionMessage` on each `protect()` call.\n\n```ts\nimport arcjet, { detectPromptInjection } from \"@arcjet/next\";\n\nconst aj = arcjet({\n  key: process.env.ARCJET_KEY!,\n  rules: [\n    detectPromptInjection({\n      mode: \"LIVE\", // Blocks requests. Use \"DRY_RUN\" to log only\n    }),\n  ],\n});\n\nexport async function POST(request: Request) {\n  const { message } = await request.json();\n\n  const decision = await aj.protect(request, {\n    detectPromptInjectionMessage: message,\n  });\n\n  if (decision.isDenied() \u0026\u0026 decision.reason.isPromptInjection()) {\n    return new Response(\n      \"Prompt injection detected — please rephrase your message\",\n      { status: 400 },\n    );\n  }\n\n  // Forward to your AI model...\n}\n```\n\n### Bot protection\n\nArcjet allows you to configure a list of bots to allow or deny. Specifying\n`allow` means all other bots are denied. An empty allow list blocks all bots.\n\nAvailable categories: `CATEGORY:ACADEMIC`, `CATEGORY:ADVERTISING`,\n`CATEGORY:AI`, `CATEGORY:AMAZON`, `CATEGORY:APPLE`, `CATEGORY:ARCHIVE`,\n`CATEGORY:BOTNET`, `CATEGORY:FEEDFETCHER`, `CATEGORY:GOOGLE`,\n`CATEGORY:META`, `CATEGORY:MICROSOFT`, `CATEGORY:MONITOR`,\n`CATEGORY:OPTIMIZER`, `CATEGORY:PREVIEW`, `CATEGORY:PROGRAMMATIC`,\n`CATEGORY:SEARCH_ENGINE`, `CATEGORY:SLACK`, `CATEGORY:SOCIAL`,\n`CATEGORY:TOOL`, `CATEGORY:UNKNOWN`, `CATEGORY:VERCEL`,\n`CATEGORY:WEBHOOK`, `CATEGORY:YAHOO`. You can also allow or deny\n[specific bots by name][bot-list].\n\n```ts\nimport arcjet, { detectBot } from \"@arcjet/next\";\nimport { isSpoofedBot } from \"@arcjet/inspect\";\n\nconst aj = arcjet({\n  key: process.env.ARCJET_KEY!,\n  rules: [\n    detectBot({\n      mode: \"LIVE\", // Blocks requests. Use \"DRY_RUN\" to log only\n      allow: [\n        \"CATEGORY:SEARCH_ENGINE\", // Google, Bing, etc\n        // Uncomment to allow these other common bot categories:\n        // \"CATEGORY:MONITOR\",  // Uptime monitoring services\n        // \"CATEGORY:PREVIEW\",  // Link previews e.g. Slack, Discord\n        // See the full list at https://arcjet.com/bot-list\n      ],\n    }),\n  ],\n});\n\nexport async function GET(request: Request) {\n  const decision = await aj.protect(request);\n\n  if (decision.isDenied() \u0026\u0026 decision.reason.isBot()) {\n    return new Response(\"No bots allowed\", { status: 403 });\n  }\n\n  // Arcjet verifies the authenticity of common bots using IP data.\n  // Verification isn't always possible, so check the results separately.\n  // https://docs.arcjet.com/bot-protection/reference#bot-verification\n  if (decision.results.some(isSpoofedBot)) {\n    return new Response(\"Forbidden\", { status: 403 });\n  }\n\n  return new Response(\"Hello world\");\n}\n```\n\nBots can be configured by [category][feature-bot-protection] and/or by\n[specific bot name][bot-list]. For example, to allow search engines and the\nOpenAI crawler, but deny all other bots:\n\n```ts\ndetectBot({\n  mode: \"LIVE\",\n  allow: [\"CATEGORY:SEARCH_ENGINE\", \"OPENAI_CRAWLER_SEARCH\"],\n});\n```\n\n### Rate limiting\n\nArcjet supports token bucket, fixed window, and sliding window algorithms.\nToken buckets are ideal for controlling AI token budgets — set `capacity` to\nthe max tokens a user can spend, `refillRate` to how many tokens are restored\nper `interval`, and deduct tokens per request via `requested` in `protect()`.\nThe `interval` accepts strings (`\"1s\"`, `\"1m\"`, `\"1h\"`, `\"1d\"`) or seconds as\na number. Use `characteristics` to track limits per user instead of per IP.\n\n```ts\nimport arcjet, { tokenBucket } from \"@arcjet/next\";\n\nconst aj = arcjet({\n  key: process.env.ARCJET_KEY!,\n  characteristics: [\"userId\"], // Track per user\n  rules: [\n    tokenBucket({\n      mode: \"LIVE\",\n      refillRate: 2_000, // Refill 2,000 tokens per hour\n      interval: \"1h\",\n      capacity: 5_000, // Maximum 5,000 tokens in the bucket\n    }),\n  ],\n});\n\nconst decision = await aj.protect(request, {\n  userId: \"user-123\",\n  requested: estimate, // Number of tokens to deduct\n});\n\nif (decision.isDenied() \u0026\u0026 decision.reason.isRateLimit()) {\n  return new Response(\"AI usage limit exceeded\", { status: 429 });\n}\n```\n\n### Sensitive information detection\n\nDetect and block PII in request content. Pass the content to scan via\n`sensitiveInfoValue` on each `protect()` call. Built-in entity types:\n`CREDIT_CARD_NUMBER`, `EMAIL`, `PHONE_NUMBER`, `IP_ADDRESS`. You can also\nprovide a custom `detect` callback for additional patterns.\n\n```ts\nimport arcjet, { sensitiveInfo } from \"@arcjet/next\";\n\nconst aj = arcjet({\n  key: process.env.ARCJET_KEY!,\n  rules: [\n    sensitiveInfo({\n      mode: \"LIVE\", // Blocks requests. Use \"DRY_RUN\" to log only\n      deny: [\"CREDIT_CARD_NUMBER\", \"EMAIL\", \"PHONE_NUMBER\"],\n    }),\n  ],\n});\n\nconst decision = await aj.protect(request, {\n  sensitiveInfoValue: userMessage,\n});\n\nif (decision.isDenied() \u0026\u0026 decision.reason.isSensitiveInfo()) {\n  return new Response(\"Sensitive information detected\", { status: 400 });\n}\n```\n\n### Shield WAF\n\nProtect your application against common web attacks, including the OWASP\nTop 10.\n\n```ts\nimport arcjet, { shield } from \"@arcjet/next\";\n\nconst aj = arcjet({\n  key: process.env.ARCJET_KEY!,\n  rules: [\n    shield({\n      mode: \"LIVE\", // Blocks requests. Use \"DRY_RUN\" to log only\n    }),\n  ],\n});\n```\n\n### Email validation\n\nValidate and verify email addresses. Deny types: `DISPOSABLE`, `FREE`,\n`NO_MX_RECORDS`, `NO_GRAVATAR`, `INVALID`.\n\n```ts\nimport arcjet, { validateEmail } from \"@arcjet/next\";\n\nconst aj = arcjet({\n  key: process.env.ARCJET_KEY!,\n  rules: [\n    validateEmail({\n      mode: \"LIVE\",\n      deny: [\"DISPOSABLE\", \"INVALID\", \"NO_MX_RECORDS\"],\n    }),\n  ],\n});\n\nconst decision = await aj.protect(request, {\n  email: \"user@example.com\",\n});\n\nif (decision.isDenied() \u0026\u0026 decision.reason.isEmail()) {\n  return new Response(\"Invalid email address\", { status: 400 });\n}\n```\n\n### Request filters\n\nFilter requests using expression-based rules against request properties (IP,\nheaders, path, method, etc.).\n\n```ts\nimport arcjet, { filter } from \"@arcjet/next\";\n\nconst aj = arcjet({\n  key: process.env.ARCJET_KEY!,\n  rules: [\n    filter({\n      mode: \"LIVE\",\n      deny: ['ip.src == \"1.2.3.4\"', 'http.request.uri.path contains \"/admin\"'],\n    }),\n  ],\n});\n```\n\n#### Block by country\n\nRestrict access to specific countries — useful for licensing, compliance, or\nregional rollouts. The `allow` list denies all countries not listed:\n\n```ts\nfilter({\n  mode: \"LIVE\",\n  // Allow only US traffic — all other countries are denied\n  allow: ['ip.src.country == \"US\"'],\n});\n```\n\n#### Block VPN and proxy traffic\n\nPrevent anonymized traffic from accessing sensitive endpoints — useful for\nfraud prevention, enforcing geo-restrictions, and reducing abuse:\n\n```ts\nfilter({\n  mode: \"LIVE\",\n  deny: [\n    \"ip.src.vpn\", // VPN services\n    \"ip.src.proxy\", // Open proxies\n    \"ip.src.tor\", // Tor exit nodes\n  ],\n});\n```\n\nFor more nuanced handling, use `decision.ip` helpers after calling `protect()`:\n\n```ts\nconst decision = await aj.protect(request);\n\nif (decision.ip.isVpn() || decision.ip.isTor()) {\n  return new Response(\"VPN traffic not allowed\", { status: 403 });\n}\n```\n\nSee the [Request Filters docs][feature-filters],\n[IP Geolocation blueprint][blueprint-ip-geolocation], and\n[VPN/Proxy Detection blueprint][blueprint-vpn-proxy] for more details.\n\n### IP analysis\n\nArcjet enriches every request with IP metadata. Use these helpers to make\npolicy decisions based on network signals:\n\n```ts\nconst decision = await aj.protect(request);\n\nif (decision.ip.isHosting()) {\n  // Requests from cloud/hosting providers are often automated.\n  // https://docs.arcjet.com/blueprints/vpn-proxy-detection\n  return new Response(\"Forbidden\", { status: 403 });\n}\n\nif (decision.ip.isVpn() || decision.ip.isProxy() || decision.ip.isTor()) {\n  // Handle VPN/proxy traffic according to your policy\n}\n\n// Access geolocation and network details\nconsole.log(decision.ip.country, decision.ip.city, decision.ip.asn);\n```\n\n### Custom characteristics\n\nTrack and limit requests by any stable identifier — user ID, API key, session,\netc. — rather than IP address alone.\n\n```ts\nconst aj = arcjet({\n  key: process.env.ARCJET_KEY!,\n  characteristics: [\"userId\"], // Declare at the SDK level\n  rules: [\n    tokenBucket({\n      mode: \"LIVE\",\n      refillRate: 2_000,\n      interval: \"1h\",\n      capacity: 5_000,\n    }),\n  ],\n});\n\n// Pass the characteristic value at request time\nconst decision = await aj.protect(request, {\n  userId: \"user-123\",\n  requested: estimate,\n});\n```\n\n## Arcjet Guard\n\n\u003e `@arcjet/guard` is a lower-level API designed for AI\n\u003e agent tool calls and background tasks where there is no HTTP request object.\n\u003e It gives you fine-grained, per-call control over rate limiting, prompt\n\u003e injection detection, sensitive information detection, and custom rules.\n\n### How it differs from framework SDKs\n\n|                    | Framework SDKs (`@arcjet/next`, etc.)             | `@arcjet/guard`                                                 |\n| ------------------ | ------------------------------------------------- | --------------------------------------------------------------- |\n| **Designed for**   | HTTP request protection                           | AI agent tool calls, background jobs                            |\n| **Request object** | Required (`protect(request, ...)`)                | Not needed                                                      |\n| **Rule binding**   | Rules configured once, input via `protect()` opts | Rules configured as functions, called with input per invocation |\n| **Rate limit key** | IP or `characteristics` dict                      | Explicit `key` string (SHA-256 hashed before sending)           |\n| **Custom rules**   | Not supported                                     | `defineCustomRule` with typed config/input/data                 |\n\n### Installation\n\n```sh\nnpm i @arcjet/guard\n```\n\n### Quick start\n\n```ts\nimport {\n  launchArcjet,\n  tokenBucket,\n  detectPromptInjection,\n} from \"@arcjet/guard\";\n\n// Create the Arcjet client once at module scope\nconst arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });\n\n// Configure reusable rules\nconst limitRule = tokenBucket({\n  refillRate: 10,\n  intervalSeconds: 60,\n  maxTokens: 100,\n});\nconst piRule = detectPromptInjection();\n\n// Per request — create rule inputs each time\nasync function handleToolCall(\n  userId: string,\n  userMessage: string,\n  tokenCount: number,\n) {\n  const rl = limitRule({ key: userId, requested: tokenCount });\n  const decision = await arcjet.guard({\n    label: \"tools.weather\",\n    rules: [rl, piRule(userMessage)],\n  });\n\n  if (decision.conclusion === \"DENY\") {\n    throw new Error(`Blocked: ${decision.reason}`);\n  }\n\n  // safe to proceed\n}\n```\n\n### Rate limiting (Guard)\n\nToken bucket, fixed window, and sliding window algorithms are available.\nConfigure the rule once, then call it with a `key` (and optional `requested`\ntoken count) for each invocation.\n\n#### Token bucket (Guard)\n\n```ts\nimport { launchArcjet, tokenBucket } from \"@arcjet/guard\";\n\nconst arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });\n\nconst limitRule = tokenBucket({\n  refillRate: 2_000, // tokens added per interval\n  intervalSeconds: 3600, // seconds between refills\n  maxTokens: 5_000, // maximum bucket capacity\n});\n\nconst decision = await arcjet.guard({\n  label: \"tools.chat\",\n  rules: [limitRule({ key: userId, requested: tokenEstimate })],\n});\n\nif (decision.conclusion === \"DENY\" \u0026\u0026 decision.reason === \"RATE_LIMIT\") {\n  throw new Error(\"Rate limit exceeded\");\n}\n```\n\n#### Fixed window (Guard)\n\n```ts\nimport { launchArcjet, fixedWindow } from \"@arcjet/guard\";\n\nconst arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });\n\nconst limitRule = fixedWindow({\n  maxRequests: 1000, // maximum requests per window\n  windowSeconds: 3600, // 1-hour window\n});\n\nconst decision = await arcjet.guard({\n  label: \"api.search\",\n  rules: [limitRule({ key: teamId })],\n});\n```\n\n#### Sliding window (Guard)\n\n```ts\nimport { launchArcjet, slidingWindow } from \"@arcjet/guard\";\n\nconst arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });\n\nconst limitRule = slidingWindow({\n  maxRequests: 500, // maximum requests per interval\n  intervalSeconds: 60, // 1-minute rolling window\n});\n\nconst decision = await arcjet.guard({\n  label: \"api.events\",\n  rules: [limitRule({ key: userId })],\n});\n```\n\n### Prompt injection detection (Guard)\n\n```ts\nimport { launchArcjet, detectPromptInjection } from \"@arcjet/guard\";\n\nconst arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });\n\nconst piRule = detectPromptInjection();\n\nconst decision = await arcjet.guard({\n  label: \"tools.chat\",\n  rules: [piRule(userMessage)],\n});\n\nif (decision.conclusion === \"DENY\" \u0026\u0026 decision.reason === \"PROMPT_INJECTION\") {\n  throw new Error(\"Prompt injection detected — please rephrase your message\");\n}\n```\n\n### Sensitive information detection (Guard)\n\nDetects PII locally — the raw text never leaves the SDK. Built-in entity\ntypes: `EMAIL`, `PHONE_NUMBER`, `IP_ADDRESS`, `CREDIT_CARD_NUMBER`.\n\n```ts\nimport { launchArcjet, localDetectSensitiveInfo } from \"@arcjet/guard\";\n\nconst arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });\n\nconst si = localDetectSensitiveInfo({\n  deny: [\"CREDIT_CARD_NUMBER\", \"PHONE_NUMBER\"],\n});\n\nconst decision = await arcjet.guard({\n  label: \"tools.summary\",\n  rules: [si(userMessage)],\n});\n\nif (decision.conclusion === \"DENY\" \u0026\u0026 decision.reason === \"SENSITIVE_INFO\") {\n  throw new Error(\"Sensitive information detected\");\n}\n```\n\n### Custom rules (Guard)\n\nDefine your own local evaluation logic with arbitrary key-value data. When\n`evaluate` is provided, the SDK calls it locally before sending the request.\nThe function receives `(config, input, { signal })` and must return\n`{ conclusion: \"ALLOW\" | \"DENY\" }`.\n\n```ts\nimport { launchArcjet, defineCustomRule } from \"@arcjet/guard\";\n\nconst arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });\n\nconst topicBlock = defineCustomRule\u003c\n  { blockedTopic: string },\n  { topic: string },\n  { matched: string }\n\u003e({\n  evaluate: (config, input) =\u003e {\n    if (input.topic === config.blockedTopic) {\n      return { conclusion: \"DENY\", data: { matched: input.topic } };\n    }\n    return { conclusion: \"ALLOW\" };\n  },\n});\n\nconst rule = topicBlock({ data: { blockedTopic: \"politics\" } });\n\nconst decision = await arcjet.guard({\n  label: \"tools.chat\",\n  rules: [rule({ data: { topic: userTopic } })],\n});\n```\n\n### Per-rule results (Guard)\n\nBoth the configured rule and the bound input provide typed result accessors:\n\n```ts\nconst limitRule = tokenBucket({\n  refillRate: 10,\n  intervalSeconds: 60,\n  maxTokens: 100,\n});\nconst rl = limitRule({ key: userId, requested: 5 });\n\nconst decision = await arcjet.guard({ label: \"tools.weather\", rules: [rl] });\n\n// From the bound input (matches exact invocation)\nconst r = rl.result(decision);\nif (r) {\n  console.log(r.remainingTokens, r.maxTokens);\n}\n\n// From the configured rule (matches all invocations of this rule)\nconst ruleResult = limitRule.result(decision);\n\n// Check only denied results\nconst denied = rl.deniedResult(decision);\nif (denied) {\n  console.log(`Rate limited — resets at ${denied.resetAtUnixSeconds}`);\n}\n```\n\nMethods available on both `RuleWithConfig` and `RuleWithInput`:\n\n| Method                   | `RuleWithConfig` (e.g. `limitRule`) | `RuleWithInput` (e.g. `rl`)        |\n| ------------------------ | ----------------------------------- | ---------------------------------- |\n| `results(decision)`      | All results for this config         | Single-element or empty array      |\n| `result(decision)`       | First result (any conclusion)       | This submission's result           |\n| `deniedResult(decision)` | First denied result                 | This submission's result if denied |\n\n### Decision API (Guard)\n\n```ts\nconst decision = await arcjet.guard({ label: \"tools.weather\", rules: [...] });\n\n// Layer 1: conclusion and reason\ndecision.conclusion;   // \"ALLOW\" or \"DENY\"\ndecision.reason;       // \"RATE_LIMIT\", \"PROMPT_INJECTION\", \"SENSITIVE_INFO\", \"CUSTOM\", \"ERROR\", etc.\n\n// Layer 2: error detection\ndecision.hasError();   // true if any rule errored or the server reported diagnostics\n\n// Layer 3: per-rule results (see \"Per-rule results\" above)\nfor (const result of decision.results) {\n  console.log(result.type, result.conclusion);\n}\n```\n\n### `guard()` parameter reference\n\n| Parameter  | Type                                  | Description                                  |\n| ---------- | ------------------------------------- | -------------------------------------------- |\n| `rules`    | `RuleWithInput[]`                     | Bound rule inputs (required)                 |\n| `label`    | `string`                              | Label identifying this guard call (required) |\n| `metadata` | `Record\u003cstring, string\u003e \\| undefined` | Optional key-value metadata                  |\n\n### DRY_RUN mode (Guard)\n\nAll guard rules accept a `mode` parameter. Use `\"DRY_RUN\"` to evaluate rules\nwithout blocking:\n\n```ts\nconst limitRule = tokenBucket({\n  mode: \"DRY_RUN\",\n  refillRate: 10,\n  intervalSeconds: 60,\n  maxTokens: 100,\n});\n```\n\n## Best practices\n\nSee the [Arcjet best practices][best-practices] for detailed guidance. Key\nrecommendations:\n\n**Create a single client instance** and reuse it across your app using\n`withRule()` to attach route-specific rules. The SDK caches decisions and\nconfiguration, so creating a new instance per request wastes that work.\n\n```ts\n// lib/arcjet.ts — create once, import everywhere\nimport arcjet, { shield } from \"@arcjet/next\";\n// Replace @arcjet/next with @arcjet/node, @arcjet/bun, etc. for your runtime\n\nexport default arcjet({\n  key: process.env.ARCJET_KEY!,\n  rules: [\n    shield({ mode: \"LIVE\" }), // base rules applied to every request\n  ],\n});\n```\n\n```ts\n// app/api/chat/route.ts — extend per-route with withRule()\nimport aj from \"@/lib/arcjet\";\nimport { detectBot, tokenBucket } from \"@arcjet/next\";\n\nconst routeAj = aj.withRule(detectBot({ mode: \"LIVE\", allow: [] })).withRule(\n  tokenBucket({\n    mode: \"LIVE\",\n    refillRate: 2_000,\n    interval: \"1h\",\n    capacity: 5_000,\n  }),\n);\n\nexport async function POST(req: Request) {\n  const decision = await routeAj.protect(req, { requested: 500 });\n  // ...\n}\n```\n\n**Other recommendations:**\n\n- **Call `protect()` in route handlers, not middleware.** Middleware lacks\n  route context, making it hard to apply route-specific rules or customize\n  responses.\n- **Call `protect()` once per request.** Calling it in both middleware and a\n  handler doubles the work and can produce unexpected results.\n- **Start rules in `DRY_RUN` mode** to observe behavior before switching to\n  `LIVE`. This lets you tune thresholds without affecting real traffic.\n- **Configure proxies** if your app runs behind a load balancer or reverse\n  proxy so Arcjet resolves the real client IP:\n  ```ts\n  arcjet({\n    key: process.env.ARCJET_KEY!,\n    rules: [],\n    proxies: [\"100.100.100.100\"],\n  });\n  ```\n- **Handle errors explicitly.** `protect()` never throws — on error it returns\n  an `ERROR` result. Fail open by logging and allowing the request:\n  ```ts\n  if (decision.isErrored()) {\n    console.error(\"Arcjet error\", decision.reason.message);\n    // allow the request to proceed\n  }\n  ```\n\n## Packages\n\nWe provide the source code for various packages in this repository, so you can\nfind a specific one through the categories and descriptions below.\n\n### SDKs\n\n- [`@arcjet/astro`](./arcjet-astro/README.md): SDK for Astro.\n- [`@arcjet/bun`](./arcjet-bun/README.md): SDK for Bun.\n- [`@arcjet/deno`](./arcjet-deno/README.md): SDK for Deno.\n- [`@arcjet/fastify`](./arcjet-fastify/README.md): SDK for Fastify.\n- [`@arcjet/guard`](./arcjet-guard/README.md): Guards SDK for AI agent tool calls and background tasks.\n- [`@arcjet/nest`](./arcjet-nest/README.md): SDK for NestJS.\n- [`@arcjet/next`](./arcjet-next/README.md): SDK for Next.js.\n- [`@arcjet/node`](./arcjet-node/README.md): SDK for Node.js.\n- [`@arcjet/nuxt`](./arcjet-nuxt/README.md): SDK for Nuxt.\n- [`@arcjet/react-router`](./arcjet-react-router/README.md): SDK for React Router.\n- [`@arcjet/remix`](./arcjet-remix/README.md): SDK for Remix.\n- [`@arcjet/sveltekit`](./arcjet-sveltekit/README.md): SDK for SvelteKit.\n\n### Nosecone\n\nSee [the docs][nosecone-docs] for details.\n\n- [`@nosecone/next`](./nosecone-next/README.md): Protect your Next.js\n  application with secure headers.\n- [`@nosecone/sveltekit`](./nosecone-sveltekit/README.md): Protect your\n  SvelteKit application with secure headers.\n- [`nosecone`](./nosecone/README.md): Protect your `Response` with secure\n  headers.\n\n### Utilities\n\n- [`@arcjet/analyze`](./analyze/README.md): Local analysis engine.\n- [`@arcjet/body`](./body/README.md): Extract the body from a stream.\n- [`@arcjet/cache`](./cache/README.md): Basic cache interface and\n  implementations.\n- [`@arcjet/decorate`](./decorate/README.md): Decorate responses with\n  info.\n- [`@arcjet/duration`](./duration/README.md): Parse duration strings.\n- [`@arcjet/env`](./env/README.md): Environment detection.\n- [`@arcjet/headers`](./headers/README.md): Extension of the Headers class.\n- [`@arcjet/inspect`](./inspect/README.md): Inspect decisions made by an SDK.\n- [`@arcjet/ip`](./ip/README.md): Find the originating IP of a request.\n- [`@arcjet/logger`](./logger/README.md): Lightweight logger which mirrors the\n  Pino structured logger interface.\n- [`@arcjet/protocol`](./protocol/README.md): JS interface into the protocol.\n- [`@arcjet/redact`](./redact/README.md): Redact \u0026 unredact sensitive info from\n  strings.\n- [`@arcjet/runtime`](./runtime/README.md): Runtime detection.\n- [`@arcjet/sprintf`](./sprintf/README.md): Platform-independent replacement\n  for `util.format`.\n- [`@arcjet/stable-hash`](./stable-hash/README.md): Stable hashing.\n- [`@arcjet/transport`](./transport/README.md): Transport mechanisms for the\n  Arcjet protocol.\n- [`arcjet`](./arcjet/README.md): JS SDK core.\n\n### Internal development\n\n- [`@arcjet/eslint-config`](./eslint-config/README.md): Custom eslint config for\n  our projects.\n- [`@arcjet/rollup-config`](./rollup-config/README.md): Custom rollup config for\n  our projects.\n\n## Support\n\nThis repository follows the [Arcjet Support Policy][arcjet-support].\n\n## Security\n\nThis repository follows the [Arcjet Security Policy][arcjet-security].\n\n## Development\n\nThis is a monorepo managed with [npm workspaces][npm-workspaces] and\n[Turborepo][turborepo]. Each package lives in its own directory at the repo\nroot (e.g. `arcjet-next/`, `analyze/`).\n\nIf you want to use Arcjet then you should install a specific package for your\nruntime (e.g. `@arcjet/next` for Next.js). If you want to contribute to the\ndevelopment of the SDKs see [CONTRIBUTING.md](./CONTRIBUTING.md).\n\n## Compatibility\n\nPackages maintained in this repository are compatible with LTS\nversions of Node.js and the current minor release of TypeScript.\n\n## License\n\nLicensed under the [Apache License, Version 2.0][apache-license].\n\n[arcjet-example]: https://example.arcjet.com\n[arcjet]: https://arcjet.com\n[github-arcjet-example-astro]: https://github.com/arcjet/example-astro\n[github-arcjet-example-deno]: https://github.com/arcjet/example-deno\n[github-arcjet-example-express]: https://github.com/arcjet/example-expressjs\n[github-arcjet-example-fastapi]: https://github.com/arcjet/example-fastapi\n[github-arcjet-example-fastify]: https://github.com/arcjet/example-fastify\n[github-arcjet-example-nestjs]: https://github.com/arcjet/example-nestjs\n[github-arcjet-example-nextjs]: https://github.com/arcjet/example-nextjs\n[github-arcjet-example-nuxt]: https://github.com/arcjet/example-nuxt\n[github-arcjet-example-react-router]: https://github.com/arcjet/example-react-router\n[github-arcjet-example-remix]: https://github.com/arcjet/example-remix\n[github-arcjet-example-sveltekit]: https://github.com/arcjet/example-sveltekit\n[github-arcjet-example-tanstack-start]: https://github.com/arcjet/example-tanstack-start\n[discord-invite]: https://arcjet.com/discord\n[support]: https://docs.arcjet.com/support\n[arcjet-docs]: https://docs.arcjet.com/\n[arcjet-support]: https://docs.arcjet.com/support\n[arcjet-security]: https://docs.arcjet.com/security\n[apache-license]: http://www.apache.org/licenses/LICENSE-2.0\n[nosecone-docs]: https://docs.arcjet.com/nosecone/quick-start\n[vercel-ai-sdk]: https://sdk.vercel.ai/\n[blueprint-ai-quota-control]: https://docs.arcjet.com/blueprints/ai-quota-control\n[blueprint-cookie-banner]: https://docs.arcjet.com/blueprints/cookie-banner\n[blueprint-custom-rule]: https://docs.arcjet.com/blueprints/defining-custom-rules\n[blueprint-ip-geolocation]: https://docs.arcjet.com/blueprints/ip-geolocation\n[blueprint-feedback-form]: https://docs.arcjet.com/blueprints/feedback-form\n[blueprint-malicious-traffic]: https://docs.arcjet.com/blueprints/malicious-traffic\n[blueprint-payment-form]: https://docs.arcjet.com/blueprints/payment-form\n[blueprint-sampling-traffic]: https://docs.arcjet.com/blueprints/sampling\n[blueprint-vpn-proxy]: https://docs.arcjet.com/blueprints/vpn-proxy-detection\n[feature-bot-protection]: https://docs.arcjet.com/bot-protection\n[feature-filters]: https://docs.arcjet.com/filters\n[feature-signup-protection]: https://docs.arcjet.com/signup-protection\n[bot-list]: https://arcjet.com/bot-list\n[best-practices]: https://docs.arcjet.com/best-practices\n[npm-workspaces]: https://docs.npmjs.com/cli/using-npm/workspaces\n[turborepo]: https://turbo.build/repo\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farcjet%2Farcjet-js","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farcjet%2Farcjet-js","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farcjet%2Farcjet-js/lists"}