Sensitive information reference for NestJS
Arcjet Sensitive Information Detection stops clients from sending you data that you don’t want to handle, such as personally identifiable information (PII). The SDK rule runs detection in your own environment with the on-device Rampart named-entity recognition (NER) model. It detects names, addresses, and government or financial identifiers, as well as structured types such as email addresses and card numbers. The text it scans never leaves your environment.
Configuration
Section titled “Configuration”You configure sensitive information detection with a list of entity types to allow or to deny. The two lists are mutually exclusive:
- With
allow, Arcjet denies any detected type that the list doesn’t name. - With
deny, Arcjet denies only the types that the list names.
Add one or more sensitive information rules to the arcjet client. Create them
with sensitiveInfo(...) in JavaScript / TypeScript,
detect_sensitive_info(...) in Python, or
arcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{...}) in Go. The lists are
allow and deny in JS and Python, and Allow and Deny in Go.
Set backend to the Rampart backend:
rampart() in JS and Python, or the Backend that you create with
rampart.New in Go.
type SensitiveInfoOptionsAllow = { mode?: "LIVE" | "DRY_RUN"; allow?: Array<ArcjetSensitiveInfoType>; // The detection backend. Pass `rampart()` from // `@arcjet/sensitive-info-rampart` (see below). backend?: SensitiveInfoBackend;};type SensitiveInfoOptionsDeny = { mode?: "LIVE" | "DRY_RUN"; deny?: Array<ArcjetSensitiveInfoType>; // The detection backend. Pass `rampart()` from // `@arcjet/sensitive-info-rampart` (see below). backend?: SensitiveInfoBackend;};# Signature for arcjet.detect_sensitive_infodef detect_sensitive_info( *, # Required. Mode.LIVE blocks requests; Mode.DRY_RUN logs only. mode: Mode, # Entity types to allow (report only). Mutually exclusive with `deny`. allow: Sequence[str | SensitiveInfoEntityType] = (), # Entity types to deny. Mutually exclusive with `allow`. deny: Sequence[str | SensitiveInfoEntityType] = (), # The detection backend. Pass `rampart()` from # `arcjet_sensitive_info_rampart` (see below). backend: SensitiveInfoBackend | None = None, # Rate-limit characteristics (see https://docs.arcjet.com/fingerprints) characteristics: Sequence[str] = (),) -> SensitiveInfoDetection: ...
# Entity types are SensitiveInfoEntityType enum members or their string# names, such as SensitiveInfoEntityType.EMAIL or "GIVEN_NAME". See the entity# types table later on this page for the full list.type SensitiveInfoOptions struct { Mode Mode Allow []EntityType // mutually exclusive with Deny Deny []EntityType Backend SensitiveInfoBackend // create with rampart.New}
// Entity types: SensitiveInfoEmail, SensitiveInfoPhoneNumber,// SensitiveInfoIPAddress, SensitiveInfoCreditCardNumber, SensitiveInfoURL,// SensitiveInfoSSN, SensitiveInfoGivenName, SensitiveInfoSurname,// SensitiveInfoTaxID, SensitiveInfoBankAccount, SensitiveInfoRoutingNumber,// SensitiveInfoGovernmentID, SensitiveInfoPassport,// SensitiveInfoDriversLicense, SensitiveInfoBuildingNumber,// SensitiveInfoStreetName, SensitiveInfoSecondaryAddress, SensitiveInfoCity,// SensitiveInfoState, SensitiveInfoZipCode.// Or deny the whole set with rampart.Entities().type ArcjetSensitiveInfoType = | "EMAIL" | "PHONE_NUMBER" | "IP_ADDRESS" | "CREDIT_CARD_NUMBER" | "GIVEN_NAME" | "SURNAME" | "SSN" | "URL" | "TAX_ID" | "BANK_ACCOUNT" | "ROUTING_NUMBER" | "GOVERNMENT_ID" | "PASSPORT" | "DRIVERS_LICENSE" | "BUILDING_NUMBER" | "STREET_NAME" | "SECONDARY_ADDRESS" | "CITY" | "STATE" | "ZIP_CODE";Guards and routes
Arcjet can be integrated into NestJS in several places using NestJS guards or directly within the route controller:
- Global guard: Applies Arcjet rules on every request, but doesn’t let you configure rules per route.
- Per route guard: Lets you configure rules per route, but requires you to add the guard to every route and has limited flexibility.
- Within route: Requires some code duplication, but allows maximum flexibility because you can customize the rules and response.
Global guard
A global guard can be configured in src/app.module.ts.
import { ArcjetModule, sensitiveInfo } from "@arcjet/nest";import { rampart } from "@arcjet/sensitive-info-rampart";import { Module } from "@nestjs/common";import { ConfigModule } from "@nestjs/config";
@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: ".env.local", }), ArcjetModule.forRoot({ isGlobal: true, key: process.env.ARCJET_KEY!, rules: [ // This allows all sensitive entities other than email addresses sensitiveInfo({ mode: "LIVE", // Will block requests, use "DRY_RUN" to log only // allow: ["EMAIL"], Will block all sensitive information types other than email. deny: ["EMAIL"], // Will block email addresses, backend: rampart(), }), ], }), // ... other modules ],})export class AppModule {}This can then be added to the controller for all the routes you wish to protect with Arcjet.
import { ArcjetGuard } from "@arcjet/nest";import { Body, Controller, Injectable, Post, Req, UseGuards,} from "@nestjs/common";
// This would normally go in your controller file, such as:// src/page/page.controller.ts@Controller("page")// Uses the ArcjetGuard to protect the controller with the default rules defined// in app.module.ts. Using a guard makes it easy to apply Arcjet rules, but you// don't get access to the decision.@UseGuards(ArcjetGuard)export class PageController { constructor(private readonly pageService: PageService) {}
@Post() async index(@Req() req: Request, @Body() body: string) { return this.pageService.message(body); }}
// This would normally go in your service file, such as:// src/page/page.service.ts@Injectable()export class PageService { message(content: string): { message: string; submittedContent: string } { return { message: "Hello world", submittedContent: content, }; }}Per route guard
A per route guard can be configured in the controller for each route you wish to
protect with specific Arcjet rules. The client created in src/app.module.ts
is automatically passed to the guard.
Arcjet applies the rules and returns a generic error if the result is DENY.
import { WithArcjetRules, sensitiveInfo } from "@arcjet/nest";import { rampart } from "@arcjet/sensitive-info-rampart";import { Body, Injectable, Post, Req } from "@nestjs/common";
// This would normally go in your controller file, such as:// src/page/page.controller.ts// Attaches the ArcjetGuard to the controller to protect it with the specified// rules extended from the global rules defined in app.module.ts.@WithArcjetRules([ // This allows all sensitive entities other than email addresses sensitiveInfo({ mode: "LIVE", // Will block requests, use "DRY_RUN" to log only // allow: ["EMAIL"], Will block all sensitive information types other than email. deny: ["EMAIL"], // Will block email addresses, backend: rampart(), }),])export class PageController { constructor(private readonly pageService: PageService) {}
@Post() async index(@Req() req: Request, @Body() body: string) { return this.pageService.message(body); }}
// This would normally go in your service file, such as:// src/page/page.service.ts@Injectable()export class PageService { message(content: string): { message: string; submittedContent: string } { return { message: "Hello world", submittedContent: content, }; }}Within route
Call Arcjet from within the route controller to have maximum flexibility.
import { ARCJET, type ArcjetNest, sensitiveInfo } from "@arcjet/nest";import { isSpoofedBot } from "@arcjet/inspect";import { rampart } from "@arcjet/sensitive-info-rampart";import { Body, Controller, HttpException, HttpStatus, Inject, Injectable, Post, Req,} from "@nestjs/common";import type { Request } from "express";
// This would normally go in your service file, such as:// src/page/page.service.ts@Injectable()export class PageService { message(content: string): { message: string; submittedContent: string } { return { message: "Hello world", submittedContent: content, }; }}
// This would normally go in your controller file, such as:// src/page/page.controller.ts@Controller("page")// Sets up the Arcjet protection without using a guard so we can access the// decision and use it in the controller.export class PageController { constructor( private readonly pageService: PageService, @Inject(ARCJET) private readonly arcjet: ArcjetNest, ) {}
@Post() async index(@Req() req: Request, @Body() body: string) { const decision = await this.arcjet .withRule( // This allows all sensitive entities other than email addresses sensitiveInfo({ mode: "LIVE", // Will block requests, use "DRY_RUN" to log only // allow: ["EMAIL"], Will block all sensitive information types other than email. deny: ["EMAIL"], // Will block email addresses, backend: rampart(), }), ) .protect(req);
if (decision.isDenied()) { if (decision.reason.isBot()) { throw new HttpException("No bots allowed", HttpStatus.FORBIDDEN); } else { throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); } }
// Paid Arcjet accounts include additional verification checks using IP data. // Verification isn't always possible, so we recommend checking the results // separately. // https://docs.arcjet.com/bot-protection/reference#bot-verification if (decision.results.some(isSpoofedBot)) { throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); }
return this.pageService.message(body); }}On-device detection with Rampart
Section titled “On-device detection with Rampart”Sensitive information detection runs on-device. It pairs the Rampart NER model, which understands context, with deterministic recognizers for structured types. The rule detects names, addresses, and government or financial identifiers, as well as email addresses, card numbers, IP addresses, and phone numbers. For the full list, see Entity types.
The model weights ship with the package, so nothing is fetched at runtime and the text you scan stays in your environment. JavaScript and Python run a ~15 MB quantized ONNX model. Go embeds the same ~15 MB of weights in a nested module and runs inference in pure Go, with no cgo or ONNX runtime.
Install the Rampart package alongside your Arcjet SDK. In Python, it ships as
the sensitive-info-rampart extra of the arcjet package. In Go, it’s a nested
module that bundles the model weights:
npm install @arcjet/sensitive-info-rampartpip install "arcjet[sensitive-info-rampart]"go get github.com/arcjet/arcjet-go/sensitiveinfo/rampartThen import rampart and pass it as the rule’s backend. Create the Go
backend once at startup with rampart.New and reuse it. List the entity types
to detect in allow or deny:
import { ARCJET, type ArcjetNest, sensitiveInfo } from "@arcjet/nest";import { rampart } from "@arcjet/sensitive-info-rampart";import { Body, Controller, HttpException, HttpStatus, Inject, Injectable, Logger, Post, Req,} from "@nestjs/common";import type { Request } from "express";
// This would normally go in your service file, such as:// src/page/page.service.ts@Injectable()export class PageService { message(content: string): { message: string; submittedContent: string } { return { message: "Hello world", submittedContent: content, }; }}
// This would normally go in your controller file, such as:// src/page/page.controller.ts@Controller("page")// Sets up the Arcjet protection without using a guard so we can access the// decision and use it in the controller.export class PageController { // Make use of the NestJS logger: https://docs.nestjs.com/techniques/logger private readonly logger = new Logger(PageController.name);
constructor( private readonly pageService: PageService, @Inject(ARCJET) private readonly arcjet: ArcjetNest, ) {}
@Post() async index(@Req() req: Request, @Body() body: string) { const decision = await this.arcjet .withRule( sensitiveInfo({ mode: "LIVE", // Will block requests, use "DRY_RUN" to log only // Rampart detects names and addresses as well as email addresses // and government identifiers. See the reference for the full list. deny: ["EMAIL", "GIVEN_NAME", "SURNAME", "STREET_NAME", "SSN"], // Run detection on-device with the Rampart NER model. backend: rampart(), }), ) .protect(req);
this.logger.log(`Arcjet: id = ${decision.id}`); this.logger.log(`Arcjet: decision = ${decision.conclusion}`);
if (decision.isDenied()) { if (decision.reason.isSensitiveInfo()) { throw new HttpException( "Unexpected sensitive info detected", HttpStatus.BAD_REQUEST, ); } else { throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); } }
// Fail closed when Arcjet couldn't complete the check. if (decision.isErrored()) { throw new HttpException( "Service unavailable", HttpStatus.SERVICE_UNAVAILABLE, ); }
return this.pageService.message(body); }}To deny every type that Rampart detects, use the full set: rampartEntities in
JS, rampart_entities in Python, or rampart.Entities() in Go:
import { rampart, rampartEntities } from "@arcjet/sensitive-info-rampart";
sensitiveInfo({ mode: "LIVE", deny: rampartEntities, backend: rampart() });from arcjet import Mode, detect_sensitive_infofrom arcjet_sensitive_info_rampart import rampart, rampart_entities
detect_sensitive_info(mode=Mode.LIVE, deny=rampart_entities, backend=rampart())backend, err := rampart.New(rampart.Options{})arcjet.SensitiveInfo(arcjet.SensitiveInfoOptions{ Mode: arcjet.ModeLive, Deny: rampart.Entities(), Backend: backend,})Model accuracy and performance
Section titled “Model accuracy and performance”Rampart is compact enough to run inline on each request: a ~14.7 MB
artifact with ~18.5M parameters, quantized to 4-bit weights, and a 512-token
context window. Longer text is scanned in overlapping windows. On the Node.js
CPU runtime, median (p50) inference is ~6.6 ms, and ~3.9 ms on
WebGPU (set device: "webgpu" in the JS SDK). The Python backend runs on
ONNX Runtime and uses the CPU by default. To use a
different accelerator, set the providers option. In Go, create the backend
once with rampart.New and reuse it.
On the model author’s held-out benchmark, Rampart recalls ~98% of private terms across the seven Latin-script languages it supports: English, Spanish, French, German, Italian, Portuguese, and Dutch.
Accuracy depends on the input. Keep the following trade-offs in mind:
- Non-Latin scripts (for example Chinese, Japanese, Korean, Arabic, Hindi, or Cyrillic) have much lower recall – the model is trained on Latin-script text.
- Government or financial identifiers without a checksum (passports, licenses, case numbers) are recognized less reliably than the structured types the deterministic recognizers validate, such as emails and card numbers.
- Latency – inference runs synchronously in the request path, so it adds to the response time of each request that the rule scans.
These figures come from the Rampart model card, which has the full per-language breakdown, calibration data, and limitations.
Rampart options
Section titled “Rampart options”The rampart() factory (JS and Python) or rampart.New (Go) accepts these
options:
import { defaultRecognizers, rampart } from "@arcjet/sensitive-info-rampart";
rampart({ // Execution device (default: "cpu"). Set to "webgpu" to use a GPU when the // runtime supports it. device: "webgpu", // Minimum confidence score for a model token to count (default: 0.5). threshold: 0.6, // Largest input to scan, in characters (default: 100_000). The rule denies // longer input without scanning it. Lower it to bound latency, or raise it // to accept longer text. maxInputChars: 100_000, // Deterministic recognizers to run alongside the model (default: // `defaultRecognizers`). This is the extension point for custom detection – // add a recognizer that returns matched spans. Pass `[]` to rely on the // model alone. recognizers: [ ...defaultRecognizers, (value) => { // Return an array of `{ start, end, type }` spans. return []; }, ],});The rampart() factory takes an optional RampartOptions:
from arcjet_sensitive_info_rampart import ( RampartOptions, default_recognizers, rampart,)
rampart( RampartOptions( # Minimum confidence score for a model token to count (default: 0.5). threshold=0.6, # ONNX Runtime execution providers (default: ("CPUExecutionProvider",)). # Set this to use a GPU or other accelerator supported by your ONNX # Runtime install. providers=("CPUExecutionProvider",), # Largest input to scan, in characters (default: 100,000). The rule # denies longer input without scanning it. Lower it to bound latency, # or raise it to accept longer text. max_input_chars=100_000, # Deterministic recognizers to run alongside the model (default: # `default_recognizers`). This is the extension point for custom # detection – add a recognizer that returns matched spans. Pass () to # rely on the model alone. recognizers=( *default_recognizers, lambda value: [], # Return a list of DetectedSpan(start, end, type). ), # Directory containing the model files (default: the bundled models). model_path=None, ))rampart.New takes rampart.Options. Inference is pure Go (no cgo). There is
no device or ONNX provider option:
backend, err := rampart.New(rampart.Options{ // Minimum confidence score for a model token to count (default: 0.5). Threshold: 0.6, // Largest input to scan, in characters (default: 4096). The rule denies // longer input without scanning it. Raise it to accept longer text if you // can afford the extra latency. MaxInputChars: 4096, // Deterministic recognizers run alongside the model (default: // rampart.DefaultRecognizers). Pass an empty non-nil slice to rely on the // model alone. Phone numbers are left to the model because their digit // shape overlaps with financial and government identifiers. // Recognizers: []rampart.Recognizer{},})Long input
Section titled “Long input”Rampart scans text on the request path, so longer text takes longer to scan. To keep that cost predictable, each SDK sets a maximum length, which you can change:
| SDK | Default limit | Option |
|---|---|---|
| JavaScript | 100,000 characters | maxInputChars |
| Python | 100,000 characters | max_input_chars |
| Go | 4,096 characters | MaxInputChars |
Go’s default is lower because the Go backend runs the model in pure Go, with no native runtime to install, and each scan takes several times longer than with the ONNX Runtime that JavaScript and Python use.
A live rule denies text longer than the limit without scanning any of it, so
nothing past the limit goes through unchecked. A rule in dry run mode doesn’t
block: with protect() the SDK logs a warning, and with Guards Arcjet records
the denial. The limit applies to protect(), to the local
sensitive information rule in Arcjet Guards, and to a LOCAL input in an
agent guard policy. JavaScript counts UTF-16 code units, and Python and Go
count Unicode code points.
With protect(), the denial has an error reason instead of a sensitive
information reason, because Arcjet didn’t detect anything. The examples on this
page block it with their catch-all denial. To return a clearer response, check
for the error reason: decision.reason.isError() in JavaScript,
decision.reason_v2.type == "ERROR" in Python, or decision.Reason.IsError()
in Go:
if (decision.isDenied() && decision.reason.isError()) { // The text is longer than the backend's scan limit. return Response.json({ error: "Message too long" }, { status: 413 });}With Guards, the rule result is a denial with no detected entity types.
Choose a limit that fits your latency budget. Each window of about 480 characters is one inference pass, so a higher limit raises the worst-case scan time for a request. The limit must be a positive whole number, and there’s no setting that turns it off. Also cap the request body size in your server or framework, so your application doesn’t read an oversized body into memory.
On-device detection suits typical request fields. For longer text, you have the following options:
- Raise the limit when the extra latency fits your budget.
- Use a server input. Send the text as a
SERVERinput to the server-side sensitive information detector in an agent guard policy, which screens values up to about 100 KB on Arcjet’s servers. Arcjet allows up to 3 seconds to screen a value over 2 KB, and a live rule fails closed if the scan doesn’t finish. Set the client timeout above 3 seconds (the default is 2), so the client doesn’t stop waiting first, and deny the request whenhasFailedOpen()is true. - Split the text into parts, for text longer than about 100 KB, and
screen each part in its own
guard()call. Each call screens up to twoSERVERinputs longer than 2 KB.
To detect protected health information (PHI), always use the server-side detector. On-device detection doesn’t detect the PHI types. For more information, see Detect protected health information (PHI).
In JavaScript and Python, the model loads on first use and is reused for every
later request. In Go, rampart.New loads and dequantizes the weights, so
create the backend once at startup and reuse it. It’s safe for concurrent use.
To detect your own entity types, add a recognizer. See Custom entity detection.
Bundlers and runtimes
Section titled “Bundlers and runtimes”In JavaScript and Python, the Rampart backend loads a native ONNX runtime and reads its model weights from disk at runtime. This affects where it runs and how you build your application:
- It requires a server runtime with filesystem and native-addon access. In JS that means Node.js, Bun, or Deno – it does not run on edge runtimes. In Python it runs anywhere the SDK does (Python 3.10+).
- A server build must not bundle it, or the runtime fails to locate the model weights.
In Go, inference is pure Go. The weights are compiled into the nested module, so there’s no ONNX runtime, native addon, or extra file for a bundler to miss. An application that imports the module adds about 15 MB of weights to its binary.
In Python, the backend depends on
ONNX Runtime (onnxruntime), which the
arcjet[sensitive-info-rampart] extra installs. Inference is synchronous, so
it adds to the response time of each request that the rule scans, with either
the arcjet() or the arcjet_sync() client.
For Next.js, mark the package and its native dependencies as
server external packages,
so that Next.js loads them from node_modules at runtime instead of bundling
them. Run each route handler that uses the backend on the Node.js runtime, which
is the default, rather than the Edge runtime:
/** @type {import('next').NextConfig} */const nextConfig = { serverExternalPackages: [ "@arcjet/sensitive-info-rampart", "@huggingface/transformers", "onnxruntime-node", ],};
module.exports = nextConfig;export const runtime = "nodejs";Decision
Section titled “Decision”To evaluate your rules, call the protect function.
If you are using a global guard or per route
guard then protect is called for you behind the scenes. If you add Arcjet
within a route then you call it directly.
The function returns a Promise that resolves to an ArcjetDecision object
with the following properties:
id(string) – The unique ID for the request. Use it to look up the request in the Arcjet Console. Decisions that involve the Arcjet cloud API have the prefixreq_, and decisions made locally have the prefixlreq_.conclusion(ArcjetConclusion) – The overall conclusion from evaluating every configured rule. To follow Arcjet’s recommended action, use this property.reason(ArcjetReason) – More detail about the conclusion.results(ArcjetRuleResult[]) – The result of each rule that ran.ip(ArcjetIpDetails) – Arcjet’s analysis of the client IP address. For more information, see the SDK reference.
To check whether a sensitive information rule denied the request, use
decision.isDenied() and decision.reason.isSensitiveInfo() (JS) /
decision.is_denied() and decision.reason_v2.type == "SENSITIVE_INFO"
(Python) / decision.IsDenied() and decision.Reason.IsSensitiveInfo() (Go).
To see what each rule found, iterate through the results:
for (const result of decision.results) { console.log("Rule Result", result);}for result in decision.results: print("Rule Result", result)for _, result := range decision.Results { log.Printf("Rule Result %+v", result)}The following example logs each result and the sensitive information rule:
import { ARCJET, type ArcjetNest, detectBot, sensitiveInfo,} from "@arcjet/nest";import { isSpoofedBot } from "@arcjet/inspect";import { rampart } from "@arcjet/sensitive-info-rampart";import { Body, Controller, HttpException, HttpStatus, Inject, Injectable, Logger, Post, Req,} from "@nestjs/common";import type { Request } from "express";
// This would normally go in your service file, such as:// src/page/page.service.ts@Injectable()export class PageService { message(content: string): { message: string; submittedContent: string } { return { message: "Hello world", submittedContent: content, }; }}
// This would normally go in your controller file, such as:// src/page/page.controller.ts@Controller("page")// Sets up the Arcjet protection without using a guard so we can access the// decision and use it in the controller.export class PageController { // Make use of the NestJS logger: https://docs.nestjs.com/techniques/logger // See // https://github.com/arcjet/example-nestjs/blob/ec742e58c8da52d0a399327182c79e3f4edc8f3b/src/app.module.ts#L29 // and https://github.com/arcjet/example-nestjs/blob/main/src/arcjet-logger.ts // for an example of how to connect Arcjet to the NestJS logger private readonly logger = new Logger(PageController.name);
constructor( private readonly pageService: PageService, @Inject(ARCJET) private readonly arcjet: ArcjetNest, ) {}
@Post() async index(@Req() req: Request, @Body() body: string) { const decision = await this.arcjet .withRule( detectBot({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only // configured with a list of bots to allow from // https://arcjet.com/bot-list allow: [], // blocks all automated clients }), ) .withRule( // This allows all sensitive entities other than email addresses sensitiveInfo({ mode: "LIVE", // Will block requests, use "DRY_RUN" to log only // allow: ["EMAIL"], Will block all sensitive information types other than email. deny: ["EMAIL"], // Will block email addresses, backend: rampart(), }), ) .protect(req);
this.logger.log(`Arcjet: id = ${decision.id}`); this.logger.log(`Arcjet: decision = ${decision.conclusion}`);
for (const result of decision.results) { this.logger.log("Rule Result", result);
if (result.reason.isSensitiveInfo()) { this.logger.log("Sensitive info rule", result); }
if (result.reason.isBot()) { this.logger.log("Bot protection rule", result); } }
if (decision.isDenied()) { if (decision.reason.isBot()) { throw new HttpException("No bots allowed", HttpStatus.FORBIDDEN); } else if (decision.reason.isSensitiveInfo()) { throw new HttpException( "Unexpected sensitive info detected", HttpStatus.BAD_REQUEST, ); } else { throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); } }
// Paid Arcjet accounts include additional verification checks using IP data. // Verification isn't always possible, so we recommend checking the decision // separately. // https://docs.arcjet.com/bot-protection/reference#bot-verification if (decision.results.some(isSpoofedBot)) { return new HttpException("Forbidden", HttpStatus.FORBIDDEN); }
return this.pageService.message(body); }}Entity types
Section titled “Entity types”For every entity type that Arcjet detects, grouped by category with its
taxonomy version and where Arcjet detects it, see
Sensitive information entity types.
The SDK detects the version 1 types with Rampart. List any of them in allow or
deny. Each one is an ArcjetSensitiveInfoType in JS, a
SensitiveInfoEntityType in Python, and an arcjet.EntityType in Go.
To list every type at once, use rampartEntities (JS), rampart_entities
(Python), or rampart.Entities() (Go).
Structured type matching
Section titled “Structured type matching”The default recognizers match these structured types with deterministic, validated patterns.
Card numbers
Section titled “Card numbers”Arcjet detects card numbers of 13 to 19 digits when the final digit is a valid
Luhn check digit. The values
4242424242424242, 4000 0566 5566 5556, and 3782-8224-6310005 match, but
4242424242424241 doesn’t. Spaces and dashes are ignored, because people often
group the digits.
Email addresses
Section titled “Email addresses”The values alice@example.com and bob.smith@subdomain.example.com match, but
alice.example.com doesn’t.
IP addresses
Section titled “IP addresses”Arcjet detects IPv4 and IPv6 addresses. The values 127.0.0.1 and ::1 match.
Social Security numbers
Section titled “Social Security numbers”Arcjet detects US Social Security numbers in the dashed form, such as
123-45-6789.
Arcjet detects URLs that start with http://, https://, or www.. The model
also detects URLs from context.
Phone numbers
Section titled “Phone numbers”The Rampart model detects phone numbers from context. The default recognizers don’t include a phone number pattern, because a run of digits can also be a bank account, routing number, or government identifier, and a pattern can’t tell which one it is.
Custom entity detection
Section titled “Custom entity detection”To detect patterns that the default recognizers don’t cover, add your own
recognizer to the Rampart recognizers option. A recognizer receives the full
text and returns the spans it matched, each with a start offset, an end offset,
and an entity type. Recognizer results take precedence over the model where
they overlap.
Keep the default recognizers in the list unless you want to replace them. The
following example adds a recognizer that reports IBANs as BANK_ACCOUNT:
import { ARCJET, type ArcjetNest, sensitiveInfo } from "@arcjet/nest";import { defaultRecognizers, type DetectedSpan, rampart,} from "@arcjet/sensitive-info-rampart";import { Body, Controller, HttpException, HttpStatus, Inject, Injectable, Logger, Post, Req,} from "@nestjs/common";import type { Request } from "express";
// This would normally go in your service file, such as:// src/page/page.service.ts@Injectable()export class PageService { message(content: string): { message: string; submittedContent: string } { return { message: "Hello world", submittedContent: content, }; }}
// This would normally go in your controller file, such as:// src/page/page.controller.ts// A recognizer receives the full text and returns the spans it matched. This// one reports IBANs as bank account numbers.function detectIban(value: string): DetectedSpan[] { const spans: DetectedSpan[] = []; for (const match of value.matchAll(/\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/g)) { spans.push({ start: match.index, end: match.index + match[0].length, type: "BANK_ACCOUNT", }); } return spans;}
@Controller("page")// Sets up the Arcjet protection without using a guard so we can access the// decision and use it in the controller.export class PageController { // Make use of the NestJS logger: https://docs.nestjs.com/techniques/logger // See // https://github.com/arcjet/example-nestjs/blob/ec742e58c8da52d0a399327182c79e3f4edc8f3b/src/app.module.ts#L29 // and https://github.com/arcjet/example-nestjs/blob/main/src/arcjet-logger.ts // for an example of how to connect Arcjet to the NestJS logger private readonly logger = new Logger(PageController.name);
constructor( private readonly pageService: PageService, @Inject(ARCJET) private readonly arcjet: ArcjetNest, ) {}
@Post() async index(@Req() req: Request, @Body() body: string) { const decision = await this.arcjet .withRule( // This allows all sensitive entities other than email addresses sensitiveInfo({ mode: "LIVE", // Will block requests, use "DRY_RUN" to log only // allow: ["EMAIL"], Will block all sensitive information types other than email. deny: ["EMAIL", "BANK_ACCOUNT"], // Will block email addresses and IBANs backend: rampart({ // Keep the default recognizers and add your own. recognizers: [...defaultRecognizers, detectIban], }), }), ) .protect(req);
this.logger.log(`Arcjet: id = ${decision.id}`); this.logger.log(`Arcjet: decision = ${decision.conclusion}`);
if (decision.isDenied()) { if (decision.reason.isSensitiveInfo()) { throw new HttpException( "Unexpected sensitive info detected", HttpStatus.BAD_REQUEST, ); } else { throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); } }
return this.pageService.message(body); }}Offsets are UTF-16 code unit indexes in JS, code point indexes in Python, and byte offsets in Go – the indexes each language’s regular expression matches return.
Error handling
Section titled “Error handling”By default, Arcjet fails open, so a service issue or misconfiguration doesn’t block your users. The SDK times out and fails open after 2,000 ms. Most responses take 20 ms to 30 ms.
If a rule hits an error, Arcjet returns an ERROR result for that rule. The
message property on the result explains what went wrong. If every other rule
that ran returned ALLOW, the overall conclusion is ERROR.
Sensitive information detection protects data, so deny the request when the decision is errored, unless your application can accept data that Arcjet didn’t screen. The quick start examples fail closed this way. The following example logs the error and shows where to fail closed:
import { ARCJET, type ArcjetNest, sensitiveInfo } from "@arcjet/nest";import { rampart } from "@arcjet/sensitive-info-rampart";import { Body, Controller, HttpException, HttpStatus, Inject, Injectable, Logger, Post, Req,} from "@nestjs/common";import type { Request } from "express";
// This would normally go in your service file, such as:// src/page/page.service.ts@Injectable()export class PageService { message(content: string): { message: string; submittedContent: string } { return { message: "Hello world", submittedContent: content, }; }}
// This would normally go in your controller file, such as:// src/page/page.controller.ts@Controller("page")// Sets up the Arcjet protection without using a guard so we can access the// decision and use it in the controller.export class PageController { // Make use of the NestJS logger: https://docs.nestjs.com/techniques/logger // See // https://github.com/arcjet/example-nestjs/blob/ec742e58c8da52d0a399327182c79e3f4edc8f3b/src/app.module.ts#L29 // and https://github.com/arcjet/example-nestjs/blob/main/src/arcjet-logger.ts // for an example of how to connect Arcjet to the NestJS logger private readonly logger = new Logger(PageController.name);
constructor( private readonly pageService: PageService, @Inject(ARCJET) private readonly arcjet: ArcjetNest, ) {}
@Post() async index(@Req() req: Request, @Body() body: string) { const decision = await this.arcjet .withRule( // This allows all sensitive entities other than email addresses sensitiveInfo({ mode: "LIVE", // Will block requests, use "DRY_RUN" to log only // allow: ["EMAIL"], Will block all sensitive information types other than email. deny: ["EMAIL"], // Will block email addresses, backend: rampart(), }), ) .protect(req);
this.logger.log(`Arcjet: id = ${decision.id}`); this.logger.log(`Arcjet: decision = ${decision.conclusion}`);
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open to prevent an Arcjet error from blocking all requests. You // may want to fail closed if this controller is very sensitive this.logger.error(`Arcjet error: ${reason.message}`); } }
if (decision.isDenied()) { if (decision.reason.isSensitiveInfo()) { throw new HttpException( "Unexpected sensitive info detected", HttpStatus.BAD_REQUEST, ); } else { throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); } }
return this.pageService.message(body); }}Testing
Section titled “Testing”Arcjet runs the same way in every environment, including locally and in CI. To
log the results of your rules without blocking any requests, set mode to
DRY_RUN.
You can test your rules automatically with a test framework, or with a sample of your traffic. For more information, see Testing.