From 1cda96600665299171d12e15e19ce785e36359a3 Mon Sep 17 00:00:00 2001 From: David Mytton Date: Mon, 2 Dec 2024 12:34:52 +0000 Subject: [PATCH] chore: Complete NestJS docs (#293) --- package-lock.json | 190 +++++++++++++++++- package.json | 4 +- src/content/docs/bot-protection/reference.mdx | 6 +- .../docs/email-validation/quick-start.mdx | 12 +- .../docs/email-validation/reference.mdx | 18 +- src/content/docs/rate-limiting/reference.mdx | 6 +- src/content/docs/reference/nestjs.mdx | 4 +- .../docs/sensitive-info/quick-start.mdx | 24 ++- src/content/docs/sensitive-info/reference.mdx | 35 +++- src/content/docs/shield/quick-start.mdx | 49 +++-- src/content/docs/shield/reference.mdx | 42 +++- .../docs/signup-protection/quick-start.mdx | 11 +- .../docs/signup-protection/reference.mdx | 23 ++- .../quick-start/nestjs/Step3.mdx | 10 +- .../reference/nestjs/DecoratorRoutes.mdx | 4 +- .../quick-start/nestjs/Step1.mdx | 20 ++ .../quick-start/nestjs/Step3.mdx | 21 ++ .../quick-start/nestjs/Step3AppModule.ts | 34 ++++ .../quick-start/nestjs/Step3Controller.ts | 101 ++++++++++ .../quick-start/nestjs/Step4.mdx | 30 +++ .../reference/nestjs/DecisionLog.mdx | 9 + .../reference/nestjs/DecisionLog.ts | 126 ++++++++++++ .../reference/nestjs/Errors.mdx | 9 + .../reference/nestjs/Errors.ts | 114 +++++++++++ src/snippets/get-started/nest-js/Step3.mdx | 9 +- .../reference/nestjs/DecoratorRoutes.mdx | 4 +- .../reference/nestjs/Configuration.ts | 2 +- .../sensitive-info/quick-start/bun/Step3.js | 5 +- .../sensitive-info/quick-start/bun/Step3.ts | 5 +- .../quick-start/nestjs/Step1.mdx | 26 +++ .../quick-start/nestjs/Step3.js | 38 ++++ .../quick-start/nestjs/Step3.mdx | 23 +++ .../quick-start/nestjs/Step3.ts | 38 ++++ .../quick-start/nestjs/Step4.mdx | 3 + .../quick-start/nextjs/Step3App.js | 2 +- .../quick-start/nextjs/Step3App.ts | 2 +- .../quick-start/nextjs/Step3Pages.js | 2 +- .../quick-start/nextjs/Step3Pages.ts | 2 +- .../quick-start/nodejs/Step3.js | 2 +- .../quick-start/nodejs/Step3.ts | 2 +- .../quick-start/remix/Step3.jsx | 2 +- .../quick-start/remix/Step3.tsx | 2 +- .../quick-start/sveltekit/Step3.js | 2 +- .../quick-start/sveltekit/Step3.ts | 2 +- .../reference/nestjs/CustomDetect.mdx | 9 + .../reference/nestjs/CustomDetect.ts | 86 ++++++++ .../reference/nestjs/DecisionLog.mdx | 9 + .../reference/nestjs/DecisionLog.ts | 101 ++++++++++ .../reference/nestjs/DecoratorRoutes.mdx | 62 ++++++ .../reference/nestjs/Errors.mdx | 9 + .../sensitive-info/reference/nestjs/Errors.ts | 87 ++++++++ .../reference/nestjs/GlobalGuard.ts | 30 +++ .../reference/nestjs/GlobalGuardRoute.ts | 37 ++++ .../reference/nestjs/PerRouteGuard.ts | 35 ++++ .../reference/nestjs/WithinRoute.ts | 60 ++++++ src/snippets/shield/quick-start/bun/Step3.js | 1 + src/snippets/shield/quick-start/bun/Step3.ts | 1 + .../shield/quick-start/nestjs/Step1.mdx | 26 +++ .../shield/quick-start/nestjs/Step3.js | 35 ++++ .../shield/quick-start/nestjs/Step3.mdx | 19 ++ .../shield/quick-start/nestjs/Step3.ts | 35 ++++ .../shield/quick-start/nestjs/Step4.mdx | 23 +++ .../shield/quick-start/nestjs/Step5.mdx | 19 ++ .../shield/quick-start/nextjs/PerRouteApp.js | 2 + .../shield/quick-start/nextjs/PerRouteApp.ts | 2 + .../quick-start/nextjs/PerRoutePages.js | 2 + .../quick-start/nextjs/PerRoutePages.ts | 2 + .../shield/quick-start/nodejs/Step3.js | 5 +- .../shield/quick-start/nodejs/Step3.ts | 5 +- .../shield/quick-start/remix/Step3.jsx | 5 +- .../shield/quick-start/remix/Step3.tsx | 5 +- .../shield/quick-start/sveltekit/Step3.js | 5 +- .../shield/quick-start/sveltekit/Step3.ts | 5 +- .../shield/reference/nestjs/DecisionLog.mdx | 9 + .../shield/reference/nestjs/DecisionLog.ts | 86 ++++++++ .../reference/nestjs/DecoratorRoutes.mdx | 65 ++++++ .../shield/reference/nestjs/Errors.mdx | 13 ++ .../shield/reference/nestjs/Errors.ts | 72 +++++++ .../shield/reference/nestjs/GlobalGuard.ts | 28 +++ .../reference/nestjs/GlobalGuardRoute.ts | 29 +++ .../shield/reference/nestjs/PerRouteGuard.ts | 31 +++ .../shield/reference/nestjs/WithinRoute.ts | 55 +++++ .../quick-start/nestjs/Step1.mdx | 26 +++ .../quick-start/nestjs/Step3.mdx | 21 ++ .../quick-start/nestjs/Step3AppModule.ts | 34 ++++ .../quick-start/nestjs/Step3Controller.ts | 124 ++++++++++++ .../quick-start/nestjs/Step4.mdx | 30 +++ .../quick-start/shared/Step2SetEnv.mdx | 8 +- .../reference/nestjs/CustomVerification.mdx | 9 + .../reference/nestjs/CustomVerification.ts | 163 +++++++++++++++ .../reference/nestjs/Errors.mdx | 9 + .../reference/nestjs/Errors.ts | 137 +++++++++++++ .../reference/nestjs/Recommended.mdx | 9 + .../reference/nestjs/Recommended.ts | 23 +++ 94 files changed, 2687 insertions(+), 91 deletions(-) create mode 100644 src/snippets/email-validation/quick-start/nestjs/Step1.mdx create mode 100644 src/snippets/email-validation/quick-start/nestjs/Step3.mdx create mode 100644 src/snippets/email-validation/quick-start/nestjs/Step3AppModule.ts create mode 100644 src/snippets/email-validation/quick-start/nestjs/Step3Controller.ts create mode 100644 src/snippets/email-validation/quick-start/nestjs/Step4.mdx create mode 100644 src/snippets/email-validation/reference/nestjs/DecisionLog.mdx create mode 100644 src/snippets/email-validation/reference/nestjs/DecisionLog.ts create mode 100644 src/snippets/email-validation/reference/nestjs/Errors.mdx create mode 100644 src/snippets/email-validation/reference/nestjs/Errors.ts create mode 100644 src/snippets/sensitive-info/quick-start/nestjs/Step1.mdx create mode 100644 src/snippets/sensitive-info/quick-start/nestjs/Step3.js create mode 100644 src/snippets/sensitive-info/quick-start/nestjs/Step3.mdx create mode 100644 src/snippets/sensitive-info/quick-start/nestjs/Step3.ts create mode 100644 src/snippets/sensitive-info/quick-start/nestjs/Step4.mdx create mode 100644 src/snippets/sensitive-info/reference/nestjs/CustomDetect.mdx create mode 100644 src/snippets/sensitive-info/reference/nestjs/CustomDetect.ts create mode 100644 src/snippets/sensitive-info/reference/nestjs/DecisionLog.mdx create mode 100644 src/snippets/sensitive-info/reference/nestjs/DecisionLog.ts create mode 100644 src/snippets/sensitive-info/reference/nestjs/DecoratorRoutes.mdx create mode 100644 src/snippets/sensitive-info/reference/nestjs/Errors.mdx create mode 100644 src/snippets/sensitive-info/reference/nestjs/Errors.ts create mode 100644 src/snippets/sensitive-info/reference/nestjs/GlobalGuard.ts create mode 100644 src/snippets/sensitive-info/reference/nestjs/GlobalGuardRoute.ts create mode 100644 src/snippets/sensitive-info/reference/nestjs/PerRouteGuard.ts create mode 100644 src/snippets/sensitive-info/reference/nestjs/WithinRoute.ts create mode 100644 src/snippets/shield/quick-start/nestjs/Step1.mdx create mode 100644 src/snippets/shield/quick-start/nestjs/Step3.js create mode 100644 src/snippets/shield/quick-start/nestjs/Step3.mdx create mode 100644 src/snippets/shield/quick-start/nestjs/Step3.ts create mode 100644 src/snippets/shield/quick-start/nestjs/Step4.mdx create mode 100644 src/snippets/shield/quick-start/nestjs/Step5.mdx create mode 100644 src/snippets/shield/reference/nestjs/DecisionLog.mdx create mode 100644 src/snippets/shield/reference/nestjs/DecisionLog.ts create mode 100644 src/snippets/shield/reference/nestjs/DecoratorRoutes.mdx create mode 100644 src/snippets/shield/reference/nestjs/Errors.mdx create mode 100644 src/snippets/shield/reference/nestjs/Errors.ts create mode 100644 src/snippets/shield/reference/nestjs/GlobalGuard.ts create mode 100644 src/snippets/shield/reference/nestjs/GlobalGuardRoute.ts create mode 100644 src/snippets/shield/reference/nestjs/PerRouteGuard.ts create mode 100644 src/snippets/shield/reference/nestjs/WithinRoute.ts create mode 100644 src/snippets/signup-protection/quick-start/nestjs/Step1.mdx create mode 100644 src/snippets/signup-protection/quick-start/nestjs/Step3.mdx create mode 100644 src/snippets/signup-protection/quick-start/nestjs/Step3AppModule.ts create mode 100644 src/snippets/signup-protection/quick-start/nestjs/Step3Controller.ts create mode 100644 src/snippets/signup-protection/quick-start/nestjs/Step4.mdx create mode 100644 src/snippets/signup-protection/reference/nestjs/CustomVerification.mdx create mode 100644 src/snippets/signup-protection/reference/nestjs/CustomVerification.ts create mode 100644 src/snippets/signup-protection/reference/nestjs/Errors.mdx create mode 100644 src/snippets/signup-protection/reference/nestjs/Errors.ts create mode 100644 src/snippets/signup-protection/reference/nestjs/Recommended.mdx create mode 100644 src/snippets/signup-protection/reference/nestjs/Recommended.ts diff --git a/package-lock.json b/package-lock.json index 91129125..882d5e5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@arcjet/env": "1.0.0-alpha.32", "@arcjet/eslint-config": "1.0.0-alpha.32", "@arcjet/headers": "1.0.0-alpha.32", - "@arcjet/nest": "^1.0.0-alpha.32", + "@arcjet/nest": "1.0.0-alpha.32", "@arcjet/next": "1.0.0-alpha.32", "@arcjet/node": "1.0.0-alpha.32", "@arcjet/protocol": "1.0.0-alpha.32", @@ -43,6 +43,7 @@ "@nestjs/common": "10.4.11", "@nestjs/config": "3.3.0", "@nestjs/core": "10.4.11", + "@nestjs/platform-express": "10.4.12", "@remix-run/node": "2.15.0", "@sveltejs/kit": "2.8.5", "ai": "4.0.9", @@ -50,6 +51,7 @@ "astro": "4.16.16", "astro-embed": "0.9.0", "astro-robots-txt": "1.0.0", + "class-validator": "0.14.1", "express": "4.21.1", "hono": "4.6.12", "nanostores": "0.11.3", @@ -3280,6 +3282,33 @@ "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "license": "0BSD" }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.12", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.12.tgz", + "integrity": "sha512-+m8BQas9mnY29Y6rZv8EUqIYwcta99/dTiGIUy48LB/+YoAyDTEHpsLd2+rpetk54niGgKJYclCZRUwRcjrYYA==", + "license": "MIT", + "dependencies": { + "body-parser": "1.20.3", + "cors": "2.8.5", + "express": "4.21.1", + "multer": "1.4.4-lts.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nestjs/platform-express/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@next/env": { "version": "14.2.15", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz", @@ -4520,6 +4549,12 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==" }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.13", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", @@ -5106,6 +5141,12 @@ "url": "https://github.com/sponsors/Simon-He95" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -5946,6 +5987,17 @@ "node": ">=8" } }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -6186,6 +6238,51 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", @@ -6240,6 +6337,25 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -9021,6 +9137,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9380,6 +9502,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.15.tgz", + "integrity": "sha512-M7+rtYi9l5RvMmHyjyoF3BHHUpXTYdJ0PezZGHNs0GyW1lO+K7jxlXpbdIb7a56h0nqLYdjIw+E+z0ciGaJP7g==", + "license": "MIT" + }, "node_modules/linkedom": { "version": "0.14.26", "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.14.26.tgz", @@ -10944,6 +11072,36 @@ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", "license": "MIT" }, + "node_modules/multer": { + "version": "1.4.4-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", + "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/multiformats": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", @@ -12139,6 +12297,12 @@ "node": ">=6" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/process-warning": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", @@ -13894,6 +14058,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typeid-js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/typeid-js/-/typeid-js-1.1.0.tgz", @@ -14293,6 +14463,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -14880,6 +15059,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", diff --git a/package.json b/package.json index 881b9d9d..004ec7ce 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@arcjet/env": "1.0.0-alpha.32", "@arcjet/eslint-config": "1.0.0-alpha.32", "@arcjet/headers": "1.0.0-alpha.32", - "@arcjet/nest": "^1.0.0-alpha.32", + "@arcjet/nest": "1.0.0-alpha.32", "@arcjet/next": "1.0.0-alpha.32", "@arcjet/node": "1.0.0-alpha.32", "@arcjet/protocol": "1.0.0-alpha.32", @@ -45,6 +45,7 @@ "@nestjs/common": "10.4.11", "@nestjs/config": "3.3.0", "@nestjs/core": "10.4.11", + "@nestjs/platform-express": "10.4.12", "@remix-run/node": "2.15.0", "@sveltejs/kit": "2.8.5", "ai": "4.0.9", @@ -52,6 +53,7 @@ "astro": "4.16.16", "astro-embed": "0.9.0", "astro-robots-txt": "1.0.0", + "class-validator": "0.14.1", "express": "4.21.1", "hono": "4.6.12", "nanostores": "0.11.3", diff --git a/src/content/docs/bot-protection/reference.mdx b/src/content/docs/bot-protection/reference.mdx index f04f0d07..601b9482 100644 --- a/src/content/docs/bot-protection/reference.mdx +++ b/src/content/docs/bot-protection/reference.mdx @@ -220,9 +220,9 @@ context as passed to the request handler.
-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. +If you are using a global [guard](https://docs.nestjs.com/guards) 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.
This function returns a `Promise` that resolves to an diff --git a/src/content/docs/email-validation/quick-start.mdx b/src/content/docs/email-validation/quick-start.mdx index 06191cb1..8d43f707 100644 --- a/src/content/docs/email-validation/quick-start.mdx +++ b/src/content/docs/email-validation/quick-start.mdx @@ -1,8 +1,9 @@ --- title: "Email validation" -description: "Quick start guide for adding Arcjet email validation to your Next.js, Node.js, Bun, or SvelteKit app." +description: "Quick start guide for adding Arcjet email validation to your Next.js, NestJS, Node.js, Bun, or SvelteKit app." frameworks: - bun + - nest-js - next-js - node-js - remix @@ -22,7 +23,7 @@ ajToc: framework: ["bun", "node-js"] - text: 4. Start app anchor: "4-start-app" - framework: ["next-js", "sveltekit"] + framework: ["nest-js", "next-js", "sveltekit"] - text: "FAQs" anchor: "faqs" - text: "What next?" @@ -36,6 +37,7 @@ import WhatIsArcjet from "/src/components/WhatIsArcjet.astro"; import SlotByFramework from "@/components/SlotByFramework"; import FrameworkName from "@/components/FrameworkName"; import Step1Bun from "@/snippets/email-validation/quick-start/bun/Step1.mdx"; +import Step1NestJs from "@/snippets/email-validation/quick-start/nestjs/Step1.mdx"; import Step1NextJs from "@/snippets/email-validation/quick-start/nextjs/Step1.mdx"; import Step1NodeJs from "@/snippets/email-validation/quick-start/nodejs/Step1.mdx"; import Step1Remix from "@/snippets/email-validation/quick-start/remix/Step1.mdx"; @@ -43,6 +45,7 @@ import Step1SvelteKit from "@/snippets/email-validation/quick-start/sveltekit/St import Step2 from "@/snippets/email-validation/quick-start/shared/Step2.mdx"; import Step2SetEnv from "@/snippets/email-validation/quick-start/shared/Step2SetEnv.mdx"; import Step3Bun from "@/snippets/email-validation/quick-start/bun/Step3.mdx"; +import Step3NestJs from "@/snippets/email-validation/quick-start/nestjs/Step3.mdx"; import Step3NextJs from "@/snippets/email-validation/quick-start/nextjs/Step3.mdx"; import Step3NodeJs from "@/snippets/email-validation/quick-start/nodejs/Step3.mdx"; import Step3Remix from "@/snippets/email-validation/quick-start/remix/Step3.mdx"; @@ -52,6 +55,7 @@ import Step4SvelteKit from "@/snippets/email-validation/quick-start/sveltekit/St import Step4Bun from "@/snippets/email-validation/quick-start/bun/Step4.mdx"; import Step4Remix from "@/snippets/email-validation/quick-start/remix/Step4.mdx"; import Step4NodeJs from "@/snippets/email-validation/quick-start/nodejs/Step4.mdx"; +import Step4NestJs from "@/snippets/email-validation/quick-start/nestjs/Step4.mdx"; import FAQs from "/src/components/FAQs.astro"; import Comments from "/src/components/Comments.astro"; @@ -72,6 +76,7 @@ In your project root, run the following command to install the SDK: + @@ -87,6 +92,7 @@ project root. + @@ -100,6 +106,7 @@ a deny decision. + @@ -108,6 +115,7 @@ a deny decision. + diff --git a/src/content/docs/email-validation/reference.mdx b/src/content/docs/email-validation/reference.mdx index ffa5531f..acd3d21b 100644 --- a/src/content/docs/email-validation/reference.mdx +++ b/src/content/docs/email-validation/reference.mdx @@ -1,8 +1,9 @@ --- title: "Email validation reference" -description: "Reference guide for adding Arcjet email validation to your Next.js, Node.js, Bun, Remix, or SvelteKit app." +description: "Reference guide for adding Arcjet email validation to your Next.js, NestJS, Node.js, Bun, Remix, or SvelteKit app." frameworks: - bun + - nest-js - next-js - node-js - remix @@ -20,12 +21,16 @@ ajToc: import { Aside } from "@astrojs/starlight/components"; import SlotByFramework from "@/components/SlotByFramework"; +import TextByFramework from "@/components/TextByFramework"; + import DecisionLogBun from "@/snippets/email-validation/reference/bun/DecisionLog.mdx"; +import DecisionLogNestJs from "@/snippets/email-validation/reference/nestjs/DecisionLog.mdx"; import DecisionLogNextJs from "@/snippets/email-validation/reference/nextjs/DecisionLog.mdx"; import DecisionLogNodeJs from "@/snippets/email-validation/reference/nodejs/DecisionLog.mdx"; import DecisionLogRemix from "@/snippets/email-validation/reference/remix/DecisionLog.mdx"; import DecisionLogSvelteKit from "@/snippets/email-validation/reference/sveltekit/DecisionLog.mdx"; import ErrorsBun from "@/snippets/email-validation/reference/bun/Errors.mdx"; +import ErrorsNestJs from "@/snippets/email-validation/reference/nestjs/Errors.mdx"; import ErrorsNextJs from "@/snippets/email-validation/reference/nextjs/Errors.mdx"; import ErrorsNodeJs from "@/snippets/email-validation/reference/nodejs/Errors.mdx"; import ErrorsRemix from "@/snippets/email-validation/reference/remix/Errors.mdx"; @@ -81,6 +86,15 @@ protection rules. This requires a `request` argument which is the request context as passed to the request handler. When configured with a `validateEmail` rule it also requires an additional `email` prop. + +
+Arcjet can be integrated using a global [guard](https://docs.nestjs.com/guards) +or per route guards. However these can't be used for email validation rules +because you can't access the decision to handle the form submission. Instead, +you call the `protect` function in your route handler. See +[bot protection](/bot-protection/quick-start) for an example where guards are used. + +
This function returns a `Promise` that resolves to an `ArcjetDecision` object. This contains the following properties: @@ -114,6 +128,7 @@ This example will log the full result as well as the email validation rule: + @@ -167,6 +182,7 @@ If there is an error condition, Arcjet will return an `ERROR` `conclusion`. + diff --git a/src/content/docs/rate-limiting/reference.mdx b/src/content/docs/rate-limiting/reference.mdx index 0e0d3597..323d9285 100644 --- a/src/content/docs/rate-limiting/reference.mdx +++ b/src/content/docs/rate-limiting/reference.mdx @@ -328,9 +328,9 @@ context as passed to the request handler.
-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. +If you are using a global [guard](https://docs.nestjs.com/guards) 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.
This function returns a `Promise` that resolves to an diff --git a/src/content/docs/reference/nestjs.mdx b/src/content/docs/reference/nestjs.mdx index ae0f13c3..fc18c83c 100644 --- a/src/content/docs/reference/nestjs.mdx +++ b/src/content/docs/reference/nestjs.mdx @@ -125,7 +125,9 @@ controllers. ## Decision -Arcjet can be integrated into NestJS in several places: +Arcjet can be integrated into NestJS in several places using NestJS +[guards](https://docs.nestjs.com/guards) or directly within the route +controller: - **Global guard:** Applies Arcjet rules on every request, but does not allow you to configure rules per route. The `protect` function is called for you diff --git a/src/content/docs/sensitive-info/quick-start.mdx b/src/content/docs/sensitive-info/quick-start.mdx index 7d057e84..adf1afe5 100644 --- a/src/content/docs/sensitive-info/quick-start.mdx +++ b/src/content/docs/sensitive-info/quick-start.mdx @@ -3,6 +3,7 @@ title: "Sensitive information quick start" description: "Quick start guide for detecting sensitive information with Arcjet in your Next.js, Node.js, Bun, Remix, or SvelteKit app." frameworks: - bun + - nest-js - next-js - node-js - remix @@ -34,26 +35,33 @@ import { LinkCard, CardGrid } from "@astrojs/starlight/components"; import WhatIsArcjet from "/src/components/WhatIsArcjet.astro"; import SlotByFramework from "@/components/SlotByFramework"; import FrameworkName from "@/components/FrameworkName"; +import FAQs from "/src/components/FAQs.astro"; +import Comments from "/src/components/Comments.astro"; + import Step1Bun from "@/snippets/sensitive-info/quick-start/bun/Step1.mdx"; +import Step1NestJs from "@/snippets/sensitive-info/quick-start/nestjs/Step1.mdx"; import Step1NextJs from "@/snippets/sensitive-info/quick-start/nextjs/Step1.mdx"; import Step1NodeJs from "@/snippets/sensitive-info/quick-start/nodejs/Step1.mdx"; import StepRemix from "@/snippets/sensitive-info/quick-start/remix/Step1.mdx"; + import Step1SvelteKit from "@/snippets/sensitive-info/quick-start/sveltekit/Step1.mdx"; import Step2 from "@/snippets/sensitive-info/quick-start/shared/Step2.mdx"; import Step2SetEnv from "@/snippets/sensitive-info/quick-start/shared/Step2SetEnv.mdx"; + import Step3Bun from "@/snippets/sensitive-info/quick-start/bun/Step3.mdx"; +import Step3NestJs from "@/snippets/sensitive-info/quick-start/nestjs/Step3.mdx"; import Step3NextJs from "@/snippets/sensitive-info/quick-start/nextjs/Step3.mdx"; import Step3NodeJs from "@/snippets/sensitive-info/quick-start/nodejs/Step3.mdx"; import Step3Remix from "@/snippets/sensitive-info/quick-start/remix/Step3.mdx"; import Step3SvelteKit from "@/snippets/sensitive-info/quick-start/sveltekit/Step3.mdx"; + import Step4 from "@/snippets/sensitive-info/quick-start/nextjs/Step4.mdx"; import Step4Bun from "@/snippets/sensitive-info/quick-start/bun/Step4.mdx"; +import Step4NestJs from "@/snippets/sensitive-info/quick-start/nestjs/Step4.mdx"; import Step4NextJs from "@/snippets/sensitive-info/quick-start/nextjs/Step4.mdx"; import Step4NodeJs from "@/snippets/sensitive-info/quick-start/nodejs/Step4.mdx"; import Step4Remix from "@/snippets/sensitive-info/quick-start/remix/Step4.mdx"; import Step4SvelteKit from "@/snippets/sensitive-info/quick-start/sveltekit/Step4.mdx"; -import FAQs from "/src/components/FAQs.astro"; -import Comments from "/src/components/Comments.astro"; Arcjet Sensitive Information Detection protects against clients sending you sensitive information such as personally identifiable information (PII) that @@ -72,6 +80,7 @@ In your project root, run the following command to install the SDK: + @@ -86,6 +95,7 @@ project root. + @@ -94,13 +104,14 @@ project root. ### 3. Protect a route -Add Arcjet to detect email address from a specific route in your app. - -You can also configure Arcjet to detect credit/debit card numbers, IP addresses, -phone numbers, and/or implement a custom detection function. +Create a sensitive info detection rule. You can also configure Arcjet to detect +credit/debit card numbers, IP addresses, phone numbers, and/or implement a +custom detection function. See the [reference](/sensitive-info/reference) for +details. + @@ -114,6 +125,7 @@ request: + diff --git a/src/content/docs/sensitive-info/reference.mdx b/src/content/docs/sensitive-info/reference.mdx index 056e978e..a0ea73c7 100644 --- a/src/content/docs/sensitive-info/reference.mdx +++ b/src/content/docs/sensitive-info/reference.mdx @@ -3,6 +3,7 @@ title: "Sensitive information reference" description: "Reference guide for detecting sensitive information (PII) with Arcjet in your Next.js, Node.js, Bun, Remix, or SvelteKit app." frameworks: - bun + - nest-js - next-js - node-js - remix @@ -10,6 +11,16 @@ frameworks: ajToc: - text: "Configuration" anchor: "configuration" + - text: "Guards and routes" + anchor: "guards-and-routes" + framework: ["nest-js"] + children: + - text: "Global guard" + anchor: "global-guard" + - text: "Per route guard" + anchor: "per-route-guard" + - text: "Within route" + anchor: "within-route" - text: "Per route vs middleware" anchor: "per-route-vs-middleware" framework: ["next-js"] @@ -44,19 +55,28 @@ ajToc: --- import SlotByFramework from "@/components/SlotByFramework"; +import TextByFramework from "@/components/TextByFramework"; + +import NestJsDecoratorRoutes from "@/snippets/sensitive-info/reference/nestjs/DecoratorRoutes.mdx"; import PerRouteVsMiddlewareNextJs from "@/snippets/sensitive-info/reference/nextjs/PerRouteVsMiddleware.mdx"; import PerRouteVsHooksSvelteKit from "@/snippets/sensitive-info/reference/sveltekit/PerRouteVsHooks.mdx"; + import DecisionLogBun from "@/snippets/sensitive-info/reference/bun/DecisionLog.mdx"; +import DecisionLogNestJs from "@/snippets/sensitive-info/reference/nestjs/DecisionLog.mdx"; import DecisionLogNextJs from "@/snippets/sensitive-info/reference/nextjs/DecisionLog.mdx"; import DecisionLogNodeJs from "@/snippets/sensitive-info/reference/nodejs/DecisionLog.mdx"; import DecisionLogRemix from "@/snippets/sensitive-info/reference/remix/DecisionLog.mdx"; import DecisionLogSvelteKit from "@/snippets/sensitive-info/reference/sveltekit/DecisionLog.mdx"; + import ErrorsBun from "@/snippets/sensitive-info/reference/bun/Errors.mdx"; +import ErrorsNestJs from "@/snippets/sensitive-info/reference/nestjs/Errors.mdx"; import ErrorsNextJs from "@/snippets/sensitive-info/reference/nextjs/Errors.mdx"; import ErrorsNodeJs from "@/snippets/sensitive-info/reference/nodejs/Errors.mdx"; import ErrorsRemix from "@/snippets/sensitive-info/reference/remix/Errors.mdx"; import ErrorsSvelteKit from "@/snippets/sensitive-info/reference/sveltekit/Errors.mdx"; + import CustomDetectBun from "@/snippets/sensitive-info/reference/bun/CustomDetect.mdx"; +import CustomDetectNestJs from "@/snippets/sensitive-info/reference/nestjs/CustomDetect.mdx"; import CustomDetectNextJs from "@/snippets/sensitive-info/reference/nextjs/CustomDetect.mdx"; import CustomDetectNodeJs from "@/snippets/sensitive-info/reference/nodejs/CustomDetect.mdx"; import CustomDetectRemix from "@/snippets/sensitive-info/reference/remix/CustomDetect.mdx"; @@ -101,6 +121,7 @@ execution ordering is automatically optimized for performance. See ::: + @@ -108,7 +129,16 @@ execution ordering is automatically optimized for performance. See ## Decision Arcjet also provides a single `protect` function that is used to execute your -protection rules. This function returns a `Promise` that resolves to an +protection rules. + + +
+If you are using a global [guard](https://docs.nestjs.com/guards) 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. + +
+This function returns a `Promise` that resolves to an `ArcjetDecision` object. This contains the following properties: - `id` (`string`) - The unique ID for the request. This can be used to look up @@ -140,6 +170,7 @@ This example will log the full result as well as the sensitive info rule: + @@ -161,6 +192,7 @@ this value. + @@ -180,6 +212,7 @@ accessing `decision.reason.message`. + diff --git a/src/content/docs/shield/quick-start.mdx b/src/content/docs/shield/quick-start.mdx index c1133209..d2ce4454 100644 --- a/src/content/docs/shield/quick-start.mdx +++ b/src/content/docs/shield/quick-start.mdx @@ -3,6 +3,7 @@ title: "Shield quick start" description: "Quick start guide for adding protection to your Next.js, Node.js, Bun, Remix, or SvelteKit app with Arcjet Shield." frameworks: - bun + - nest-js - next-js - node-js - remix @@ -22,7 +23,7 @@ ajToc: framework: ["bun", "node-js"] - text: 4. Start app anchor: "4-start-app" - framework: ["next-js", "remix", "sveltekit"] + framework: ["nest-js", "next-js", "remix", "sveltekit"] - text: 5. Simulate a suspicious request anchor: "5-simulate-a-suspicious-request" - text: "FAQs" @@ -33,34 +34,39 @@ ajToc: anchor: "get-help" --- -import { LinkCard, CardGrid } from "@astrojs/starlight/components"; -import WhatIsArcjet from "/src/components/WhatIsArcjet.astro"; -import SlotByFramework from "@/components/SlotByFramework"; import FrameworkName from "@/components/FrameworkName"; +import SlotByFramework from "@/components/SlotByFramework"; +import { CardGrid, LinkCard } from "@astrojs/starlight/components"; +import Comments from "/src/components/Comments.astro"; +import FAQs from "/src/components/FAQs.astro"; +import WhatIsArcjet from "/src/components/WhatIsArcjet.astro"; + import Step1Bun from "@/snippets/shield/quick-start/bun/Step1.mdx"; +import Step3Bun from "@/snippets/shield/quick-start/bun/Step3.mdx"; +import Step4Bun from "@/snippets/shield/quick-start/bun/Step4.mdx"; +import Step5Bun from "@/snippets/shield/quick-start/bun/Step5.mdx"; +import Step1NestJs from "@/snippets/shield/quick-start/nestjs/Step1.mdx"; +import Step3NestJs from "@/snippets/shield/quick-start/nestjs/Step3.mdx"; +import Step4NestJs from "@/snippets/shield/quick-start/nestjs/Step4.mdx"; +import Step5NestJs from "@/snippets/shield/quick-start/nestjs/Step5.mdx"; import Step1NextJs from "@/snippets/shield/quick-start/nextjs/Step1.mdx"; +import Step3NextJs from "@/snippets/shield/quick-start/nextjs/Step3.mdx"; +import Step4 from "@/snippets/shield/quick-start/nextjs/Step4.mdx"; +import Step5 from "@/snippets/shield/quick-start/nextjs/Step5.mdx"; import Step1NodeJs from "@/snippets/shield/quick-start/nodejs/Step1.mdx"; +import Step3NodeJs from "@/snippets/shield/quick-start/nodejs/Step3.mdx"; +import Step4NodeJs from "@/snippets/shield/quick-start/nodejs/Step4.mdx"; +import Step5NodeJs from "@/snippets/shield/quick-start/nodejs/Step5.mdx"; import Step1Remix from "@/snippets/shield/quick-start/remix/Step1.mdx"; -import Step1SvelteKit from "@/snippets/shield/quick-start/sveltekit/Step1.mdx"; +import Step3Remix from "@/snippets/shield/quick-start/remix/Step3.mdx"; +import Step4Remix from "@/snippets/shield/quick-start/remix/Step4.mdx"; +import Step5Remix from "@/snippets/shield/quick-start/remix/Step5.mdx"; import Step2 from "@/snippets/shield/quick-start/shared/Step2.mdx"; import Step2SetEnv from "@/snippets/shield/quick-start/shared/Step2SetEnv.mdx"; -import Step3Bun from "@/snippets/shield/quick-start/bun/Step3.mdx"; -import Step3NextJs from "@/snippets/shield/quick-start/nextjs/Step3.mdx"; -import Step3NodeJs from "@/snippets/shield/quick-start/nodejs/Step3.mdx"; -import Step3Remix from "@/snippets/shield/quick-start/remix/Step3.mdx"; +import Step1SvelteKit from "@/snippets/shield/quick-start/sveltekit/Step1.mdx"; import Step3SvelteKit from "@/snippets/shield/quick-start/sveltekit/Step3.mdx"; -import Step4 from "@/snippets/shield/quick-start/nextjs/Step4.mdx"; -import Step4Remix from "@/snippets/shield/quick-start/remix/Step4.mdx"; import Step4SvelteKit from "@/snippets/shield/quick-start/sveltekit/Step4.mdx"; -import Step4Bun from "@/snippets/shield/quick-start/bun/Step4.mdx"; -import Step4NodeJs from "@/snippets/shield/quick-start/nodejs/Step4.mdx"; -import Step5 from "@/snippets/shield/quick-start/nextjs/Step5.mdx"; import Step5SvelteKit from "@/snippets/shield/quick-start/sveltekit/Step5.mdx"; -import Step5Remix from "@/snippets/shield/quick-start/remix/Step5.mdx"; -import Step5Bun from "@/snippets/shield/quick-start/bun/Step5.mdx"; -import Step5NodeJs from "@/snippets/shield/quick-start/nodejs/Step5.mdx"; -import FAQs from "/src/components/FAQs.astro"; -import Comments from "/src/components/Comments.astro"; Arcjet Shield protects your application against common attacks, including the [OWASP Top 10](https://owasp.org/www-project-top-ten/). @@ -78,6 +84,7 @@ In your project root, run the following command to install the SDK: + @@ -92,6 +99,7 @@ project root. + @@ -102,6 +110,7 @@ project root. + @@ -110,6 +119,7 @@ project root. + @@ -125,6 +135,7 @@ reached and is a constant, so you can use it as part of your tests. + diff --git a/src/content/docs/shield/reference.mdx b/src/content/docs/shield/reference.mdx index fbf4d3e5..8a1c21fa 100644 --- a/src/content/docs/shield/reference.mdx +++ b/src/content/docs/shield/reference.mdx @@ -3,6 +3,7 @@ title: "Shield reference" description: "Reference guide for adding Arcjet Shield to your Next.js, Node.js, Bun, Remix, or SvelteKit app." frameworks: - bun + - nest-js - next-js - node-js - remix @@ -10,6 +11,16 @@ frameworks: ajToc: - text: "Configuration" anchor: "configuration" + - text: "Guards and routes" + anchor: "guards-and-routes" + framework: ["nest-js"] + children: + - text: "Global guard" + anchor: "global-guard" + - text: "Per route guard" + anchor: "per-route-guard" + - text: "Within route" + anchor: "within-route" - text: "Per route vs middleware" anchor: "per-route-vs-middleware" framework: ["next-js"] @@ -45,19 +56,24 @@ ajToc: --- import SlotByFramework from "@/components/SlotByFramework"; +import TextByFramework from "@/components/TextByFramework"; + import DecisionLogBun from "@/snippets/shield/reference/bun/DecisionLog.mdx"; import ErrorsBun from "@/snippets/shield/reference/bun/Errors.mdx"; -import DecisionLogRemix from "@/snippets/shield/reference/remix/DecisionLog.mdx"; +import DecisionLogNestJs from "@/snippets/shield/reference/nestjs/DecisionLog.mdx"; +import NestJsDecoratorRoutes from "@/snippets/shield/reference/nestjs/DecoratorRoutes.mdx"; +import ErrorsNestJs from "@/snippets/shield/reference/nestjs/Errors.mdx"; import DecisionLogNextJs from "@/snippets/shield/reference/nextjs/DecisionLog.mdx"; import ErrorsNextJs from "@/snippets/shield/reference/nextjs/Errors.mdx"; +import PerRouteVsMiddlewareNextJs from "@/snippets/shield/reference/nextjs/PerRouteVsMiddleware.mdx"; import DecisionLogNodeJs from "@/snippets/shield/reference/nodejs/DecisionLog.mdx"; import ErrorsNodeJs from "@/snippets/shield/reference/nodejs/Errors.mdx"; +import DecisionLogRemix from "@/snippets/shield/reference/remix/DecisionLog.mdx"; +import ErrorsRemix from "@/snippets/shield/reference/remix/Errors.mdx"; +import RemixLoaderVsAction from "@/snippets/shield/reference/remix/LoaderVsAction.mdx"; import DecisionLogSvelteKit from "@/snippets/shield/reference/sveltekit/DecisionLog.mdx"; import ErrorsSvelteKit from "@/snippets/shield/reference/sveltekit/Errors.mdx"; -import ErrorsRemix from "@/snippets/shield/reference/remix/Errors.mdx"; -import PerRouteVsMiddlewareNextJs from "@/snippets/shield/reference/nextjs/PerRouteVsMiddleware.mdx"; import PerRouteVsHooksSvelteKit from "@/snippets/shield/reference/sveltekit/PerRouteVsHooks.mdx"; -import RemixLoaderVsAction from "@/snippets/shield/reference/remix/LoaderVsAction.mdx"; Arcjet Shield protects your application against common attacks, including the [OWASP Top 10](https://owasp.org/www-project-top-ten/). @@ -84,6 +100,7 @@ execution ordering is automatically optimized for performance. See ::: + @@ -91,14 +108,17 @@ execution ordering is automatically optimized for performance. See ## Decision -The [quick start example](/shield/quick-start?f=next-js) will deny requests -that are determined to be suspicious, immediately returning a response to the -client using Next.js middleware. - -Arcjet also provides a single `protect` function that is used to execute your -protection rules. This requires a `request` argument which is the request +Arcjet provides a single `protect` function that is used to execute your +protection rules. This requires a `RequestEvent` property which is the event context as passed to the request handler. + +
+If you are using a global [guard](https://docs.nestjs.com/guards) 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. + +
This function returns a `Promise` that resolves to an `ArcjetDecision` object. This contains the following properties: @@ -131,6 +151,7 @@ This example will log the full result as well as the shield rule: + @@ -150,6 +171,7 @@ accessing `decision.reason.message`. + diff --git a/src/content/docs/signup-protection/quick-start.mdx b/src/content/docs/signup-protection/quick-start.mdx index 4b5e1ee1..c4f09195 100644 --- a/src/content/docs/signup-protection/quick-start.mdx +++ b/src/content/docs/signup-protection/quick-start.mdx @@ -3,6 +3,7 @@ title: "Signup form protection quick start" description: "Quick start guide to protect your signup forms from abuse to your Next.js, Node.js, Bun, Remix, or SvelteKit app with Arcjet Signup form protection." frameworks: - bun + - nest-js - next-js - node-js - remix @@ -22,7 +23,7 @@ ajToc: framework: ["bun", "node-js"] - text: 4. Start app anchor: "4-start-app" - framework: ["next-js", "remix", "sveltekit"] + framework: ["nest-js", "next-js", "remix", "sveltekit"] - text: "FAQs" anchor: "faqs" - text: "What next?" @@ -35,7 +36,9 @@ import { LinkCard, CardGrid } from "@astrojs/starlight/components"; import WhatIsArcjet from "/src/components/WhatIsArcjet.astro"; import SlotByFramework from "@/components/SlotByFramework"; import FrameworkName from "@/components/FrameworkName"; + import Step1Bun from "@/snippets/signup-protection/quick-start/bun/Step1.mdx"; +import Step1NestJs from "@/snippets/signup-protection/quick-start/nestjs/Step1.mdx"; import Step1NextJs from "@/snippets/signup-protection/quick-start/nextjs/Step1.mdx"; import Step1NodeJs from "@/snippets/signup-protection/quick-start/nodejs/Step1.mdx"; import Step1Remix from "@/snippets/signup-protection/quick-start/remix/Step1.mdx"; @@ -43,6 +46,7 @@ import Step1SvelteKit from "@/snippets/signup-protection/quick-start/sveltekit/S import Step2 from "@/snippets/signup-protection/quick-start/shared/Step2.mdx"; import Step2SetEnv from "@/snippets/signup-protection/quick-start/shared/Step2SetEnv.mdx"; import Step3Bun from "@/snippets/signup-protection/quick-start/bun/Step3.mdx"; +import Step3NestJs from "@/snippets/signup-protection/quick-start/nestjs/Step3.mdx"; import Step3NextJs from "@/snippets/signup-protection/quick-start/nextjs/Step3.mdx"; import Step3NodeJs from "@/snippets/signup-protection/quick-start/nodejs/Step3.mdx"; import Step3Remix from "@/snippets/signup-protection/quick-start/remix/Step3.mdx"; @@ -51,6 +55,7 @@ import Step4 from "@/snippets/signup-protection/quick-start/nextjs/Step4.mdx"; import Step4SvelteKit from "@/snippets/signup-protection/quick-start/sveltekit/Step4.mdx"; import Step4Bun from "@/snippets/signup-protection/quick-start/bun/Step4.mdx"; import Step4NodeJs from "@/snippets/signup-protection/quick-start/nodejs/Step4.mdx"; +import Step4NestJs from "@/snippets/signup-protection/quick-start/nestjs/Step4.mdx"; import Step4Remix from "@/snippets/signup-protection/quick-start/remix/Step4.mdx"; import FAQs from "/src/components/FAQs.astro"; import Comments from "/src/components/Comments.astro"; @@ -69,6 +74,7 @@ In your project root, run the following command to install the SDK: + @@ -82,6 +88,7 @@ instructions to add a site and get a key. Add it to a `.env.local` file in your + @@ -101,6 +108,7 @@ signup form. + @@ -109,6 +117,7 @@ signup form. + diff --git a/src/content/docs/signup-protection/reference.mdx b/src/content/docs/signup-protection/reference.mdx index e2a1150b..3ea727da 100644 --- a/src/content/docs/signup-protection/reference.mdx +++ b/src/content/docs/signup-protection/reference.mdx @@ -3,6 +3,7 @@ title: "Signup from protection reference" description: "Reference guide for adding Arcjet email validation to your Next.js, Node.js, Bun, Remix, or SvelteKit app." frameworks: - bun + - nest-js - next-js - node-js - remix @@ -26,22 +27,29 @@ ajToc: --- import SlotByFramework from "@/components/SlotByFramework"; +import TextByFramework from "@/components/TextByFramework"; +import Comments from "/src/components/Comments.astro"; + import RecommendedBun from "@/snippets/signup-protection/reference/bun/Recommended.mdx"; +import RecommendedNestJs from "@/snippets/signup-protection/reference/nestjs/Recommended.mdx"; import RecommendedNextJs from "@/snippets/signup-protection/reference/nextjs/Recommended.mdx"; import RecommendedNodeJs from "@/snippets/signup-protection/reference/nodejs/Recommended.mdx"; import RecommendedRemix from "@/snippets/signup-protection/reference/remix/Recommended.mdx"; import RecommendedSvelteKit from "@/snippets/signup-protection/reference/sveltekit/Recommended.mdx"; + import CustomVerificationBun from "@/snippets/signup-protection/reference/bun/CustomVerification.mdx"; +import CustomVerificationNestJs from "@/snippets/signup-protection/reference/nestjs/CustomVerification.mdx"; import CustomVerificationNextJs from "@/snippets/signup-protection/reference/nextjs/CustomVerification.mdx"; import CustomVerificationNodeJs from "@/snippets/signup-protection/reference/nodejs/CustomVerification.mdx"; import CustomVerificationRemix from "@/snippets/signup-protection/reference/remix/CustomVerification.mdx"; import CustomVerificationSvelteKit from "@/snippets/signup-protection/reference/sveltekit/CustomVerification.mdx"; + import ErrorsBun from "@/snippets/signup-protection/reference/bun/Errors.mdx"; +import ErrorsNestJs from "@/snippets/signup-protection/reference/nestjs/Errors.mdx"; import ErrorsNextJs from "@/snippets/signup-protection/reference/nextjs/Errors.mdx"; import ErrorsNodeJs from "@/snippets/signup-protection/reference/nodejs/Errors.mdx"; import ErrorsRemix from "@/snippets/signup-protection/reference/remix/Errors.mdx"; import ErrorsSvelteKit from "@/snippets/signup-protection/reference/sveltekit/Errors.mdx"; -import Comments from "/src/components/Comments.astro"; Arcjet signup form protection combines rate limiting, bot protection, and email validation to protect your signup forms from abuse. @@ -84,6 +92,7 @@ This can be configured as follows: + @@ -109,6 +118,16 @@ protection rules. This requires a `request` argument which is the request context as passed to the request handler. When configured with a `protectSignup` rule it also requires an additional `email` prop. + +
+Arcjet can be integrated using a global [guard](https://docs.nestjs.com/guards) +or per route guards. However these can't be used for signup protection rules +because you can't access the decision to handle the form submission. Instead, +you call the `protect` function in your route handler. See +[bot protection](/bot-protection/quick-start) for an example where guards are +used. + +
This function returns a `Promise` that resolves to an `ArcjetDecision` object. This contains the following properties: @@ -159,6 +178,7 @@ users who sign up with a free email address. + @@ -176,6 +196,7 @@ If there is an error condition, Arcjet will return an `ERROR` `conclusion`. + diff --git a/src/snippets/bot-protection/quick-start/nestjs/Step3.mdx b/src/snippets/bot-protection/quick-start/nestjs/Step3.mdx index 30ebccde..c265fb83 100644 --- a/src/snippets/bot-protection/quick-start/nestjs/Step3.mdx +++ b/src/snippets/bot-protection/quick-start/nestjs/Step3.mdx @@ -6,11 +6,11 @@ import SelectableContent from "@/components/SelectableContent"; The example below will return a 403 Forbidden response for all requests from clients we are sure are automated. -This creates a global guard that will be applied to all routes. In a real -application, implementing guards or per-route protections would give you more -flexibility. See the [reference guide](/bot-protection/reference) and the -[NestJS example app](https://github.com/arcjet/example-nestjs) for how to do -this. +This creates a global [guard](https://docs.nestjs.com/guards) that will be +applied to all routes. In a real application, implementing guards or per-route +protections would give you more flexibility. See the [reference +guide](/bot-protection/reference) and the [NestJS example +app](https://github.com/arcjet/example-nestjs) for how to do this.
diff --git a/src/snippets/bot-protection/reference/nestjs/DecoratorRoutes.mdx b/src/snippets/bot-protection/reference/nestjs/DecoratorRoutes.mdx index 4780d11a..91ffdf52 100644 --- a/src/snippets/bot-protection/reference/nestjs/DecoratorRoutes.mdx +++ b/src/snippets/bot-protection/reference/nestjs/DecoratorRoutes.mdx @@ -7,7 +7,9 @@ import WithinRoute from "./WithinRoute.ts?raw"; ## Guards and routes -Arcjet can be integrated into NestJS in several places: +Arcjet can be integrated into NestJS in several places using NestJS +[guards](https://docs.nestjs.com/guards) or directly within the route +controller: - **Global guard:** Applies Arcjet rules on every request, but does not allow you to configure rules per route. diff --git a/src/snippets/email-validation/quick-start/nestjs/Step1.mdx b/src/snippets/email-validation/quick-start/nestjs/Step1.mdx new file mode 100644 index 00000000..d2f7bfd4 --- /dev/null +++ b/src/snippets/email-validation/quick-start/nestjs/Step1.mdx @@ -0,0 +1,20 @@ +import SelectableContent from "@/components/SelectableContent"; + +{/* prettier-ignore */} + +
+```sh +npm i @arcjet/nest +``` +
+
+```sh +pnpm add @arcjet/nest +``` +
+
+```sh +yarn add @arcjet/nest +``` +
+
diff --git a/src/snippets/email-validation/quick-start/nestjs/Step3.mdx b/src/snippets/email-validation/quick-start/nestjs/Step3.mdx new file mode 100644 index 00000000..d1adf78d --- /dev/null +++ b/src/snippets/email-validation/quick-start/nestjs/Step3.mdx @@ -0,0 +1,21 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import Step3AppModuleTS from "./Step3AppModule.ts?raw"; +import Step3ControllerTS from "./Step3Controller.ts?raw"; + +Several files are combined here to demonstrate creating a form handler +controller. In a real application you should split them as suggested in the +comments. + + +
+ +
+
+ +
+
diff --git a/src/snippets/email-validation/quick-start/nestjs/Step3AppModule.ts b/src/snippets/email-validation/quick-start/nestjs/Step3AppModule.ts new file mode 100644 index 00000000..78809634 --- /dev/null +++ b/src/snippets/email-validation/quick-start/nestjs/Step3AppModule.ts @@ -0,0 +1,34 @@ +import { ArcjetGuard, ArcjetModule } from "@arcjet/nest"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { APP_GUARD, NestFactory } from "@nestjs/core"; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + ArcjetModule.forRoot({ + isGlobal: true, + key: process.env.ARCJET_KEY!, + rules: [ + // We're not adding any rules here for this example, but if you did they + // would be the default rules wherever you use Arcjet. + ], + }), + ], + controllers: [], + providers: [ + { + provide: APP_GUARD, + useClass: ArcjetGuard, + }, + ], +}) +class AppModule {} + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); +} +bootstrap(); diff --git a/src/snippets/email-validation/quick-start/nestjs/Step3Controller.ts b/src/snippets/email-validation/quick-start/nestjs/Step3Controller.ts new file mode 100644 index 00000000..21345d1c --- /dev/null +++ b/src/snippets/email-validation/quick-start/nestjs/Step3Controller.ts @@ -0,0 +1,101 @@ +import { ARCJET, type ArcjetNest, validateEmail } from "@arcjet/nest"; +import { + Body, + Controller, + HttpException, + HttpStatus, + Inject, + Injectable, + Logger, + Post, + Req, + UseInterceptors, +} from "@nestjs/common"; +import { NoFilesInterceptor } from "@nestjs/platform-express"; +import { IsNotEmpty } from "class-validator"; +import type { Request } from "express"; + +// Validation class as described at +// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail +// decorator here because Arcjet handles this for you. +export class SignupDto { + @IsNotEmpty() + // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned + email: string; +} + +// This would normally go in your service file e.g. +// src/signup/signup.service.ts +@Injectable() +export class SignupService { + private readonly logger = new Logger(SignupService.name); + + signup(email: string): { message: string } { + this.logger.log(`Form submission: ${email}`); + + return { + message: "Hello world", + }; + } +} + +// This would normally go in your controller file e.g. +// src/signup/signup.controller.ts +@Controller("signup") +export class SignupController { + private readonly logger = new Logger(SignupController.name); + + constructor( + private readonly signupService: SignupService, + @Inject(ARCJET) private readonly arcjet: ArcjetNest, + ) {} + + // Implement a form handler following + // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't + // compatible with the NestJS Fastify adapter. + @Post() + @UseInterceptors(NoFilesInterceptor()) + async index(@Req() req: Request, @Body() body: SignupDto) { + const decision = await this.arcjet + .withRule( + validateEmail({ + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + // block disposable, invalid, and email addresses with no MX records + block: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], + }), + ) + .protect(req, { email: body.email }); + + this.logger.log(`Arcjet: id = ${decision.id}`); + this.logger.log(`Arcjet: decision = ${decision.conclusion}`); + + if (decision.isDenied()) { + if (decision.reason.isEmail()) { + this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`); + + let message: string; + + // These are specific errors to help the user, but will also reveal the + // validation to a spammer. + if (decision.reason.emailTypes.includes("INVALID")) { + message = "email address format is invalid. Is there a typo?"; + } else if (decision.reason.emailTypes.includes("DISPOSABLE")) { + message = "we do not allow disposable email addresses."; + } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) { + message = + "your email domain does not have an MX record. Is there a typo?"; + } else { + // This is a catch all, but the above should be exhaustive based on the + // configured rules. + message = "invalid email."; + } + + throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST); + } else { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); + } + } + + return this.signupService.signup(body.email); + } +} diff --git a/src/snippets/email-validation/quick-start/nestjs/Step4.mdx b/src/snippets/email-validation/quick-start/nestjs/Step4.mdx new file mode 100644 index 00000000..7ee1b731 --- /dev/null +++ b/src/snippets/email-validation/quick-start/nestjs/Step4.mdx @@ -0,0 +1,30 @@ +import { Aside } from "@astrojs/starlight/components"; +import SelectableContent from "@/components/SelectableContent"; + +### 4. Start app + +{/* prettier-ignore */} + +
+```sh +npm run start +``` +
+
+```sh +pnpm run start +``` +
+
+```sh +yarn run start +``` +
+
+ +Make a `curl` `POST` request from your terminal to your application with various +emails to test the result. + +```shell +curl -X POST -d 'email=test@arcjet.io' http://localhost:3000/signup/ +``` diff --git a/src/snippets/email-validation/reference/nestjs/DecisionLog.mdx b/src/snippets/email-validation/reference/nestjs/DecisionLog.mdx new file mode 100644 index 00000000..c4979e93 --- /dev/null +++ b/src/snippets/email-validation/reference/nestjs/DecisionLog.mdx @@ -0,0 +1,9 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import DecisionLogTS from "./DecisionLog.ts?raw"; + + +
+ +
+
diff --git a/src/snippets/email-validation/reference/nestjs/DecisionLog.ts b/src/snippets/email-validation/reference/nestjs/DecisionLog.ts new file mode 100644 index 00000000..4e96bcb5 --- /dev/null +++ b/src/snippets/email-validation/reference/nestjs/DecisionLog.ts @@ -0,0 +1,126 @@ +import { + ARCJET, + type ArcjetNest, + detectBot, + validateEmail, +} from "@arcjet/nest"; +import { + Body, + Controller, + HttpException, + HttpStatus, + Inject, + Injectable, + Logger, + Post, + Req, + UseInterceptors, +} from "@nestjs/common"; +import { NoFilesInterceptor } from "@nestjs/platform-express"; +import { IsNotEmpty } from "class-validator"; +import type { Request } from "express"; + +// Validation class as described at +// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail +// decorator here because Arcjet handles this for you. +export class SignupDto { + @IsNotEmpty() + // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned + email: string; +} + +// This would normally go in your service file e.g. +// src/signup/signup.service.ts +@Injectable() +export class SignupService { + private readonly logger = new Logger(SignupService.name); + + signup(email: string): { message: string } { + this.logger.log(`Form submission: ${email}`); + + return { + message: "Hello world", + }; + } +} + +// This would normally go in your controller file e.g. +// src/signup/signup.controller.ts +@Controller("signup") +export class SignupController { + private readonly logger = new Logger(SignupController.name); + + constructor( + private readonly signupService: SignupService, + @Inject(ARCJET) private readonly arcjet: ArcjetNest, + ) {} + + // Implement a form handler following + // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't + // compatible with the NestJS Fastify adapter. + @Post() + @UseInterceptors(NoFilesInterceptor()) + async index(@Req() req: Request, @Body() body: SignupDto) { + 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( + validateEmail({ + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + // block disposable, invalid, and email addresses with no MX records + block: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], + }), + ) + .protect(req, { email: body.email }); + + 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.isBot()) { + this.logger.log("Bot protection rule", result); + } + + if (result.reason.isEmail()) { + this.logger.log("Email validation rule", result); + } + } + + if (decision.isDenied()) { + if (decision.reason.isEmail()) { + this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`); + + let message: string; + + // These are specific errors to help the user, but will also reveal the + // validation to a spammer. + if (decision.reason.emailTypes.includes("INVALID")) { + message = "email address format is invalid. Is there a typo?"; + } else if (decision.reason.emailTypes.includes("DISPOSABLE")) { + message = "we do not allow disposable email addresses."; + } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) { + message = + "your email domain does not have an MX record. Is there a typo?"; + } else { + // This is a catch all, but the above should be exhaustive based on the + // configured rules. + message = "invalid email."; + } + + throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST); + } else { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); + } + } + + return this.signupService.signup(body.email); + } +} diff --git a/src/snippets/email-validation/reference/nestjs/Errors.mdx b/src/snippets/email-validation/reference/nestjs/Errors.mdx new file mode 100644 index 00000000..1ee27666 --- /dev/null +++ b/src/snippets/email-validation/reference/nestjs/Errors.mdx @@ -0,0 +1,9 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import ErrorsTS from "./Errors.ts?raw"; + + +
+ +
+
diff --git a/src/snippets/email-validation/reference/nestjs/Errors.ts b/src/snippets/email-validation/reference/nestjs/Errors.ts new file mode 100644 index 00000000..10dde00e --- /dev/null +++ b/src/snippets/email-validation/reference/nestjs/Errors.ts @@ -0,0 +1,114 @@ +import { ARCJET, type ArcjetNest, validateEmail } from "@arcjet/nest"; +import { + Body, + Controller, + HttpException, + HttpStatus, + Inject, + Injectable, + Logger, + Post, + Req, + UseInterceptors, +} from "@nestjs/common"; +import { NoFilesInterceptor } from "@nestjs/platform-express"; +import { IsNotEmpty } from "class-validator"; +import type { Request } from "express"; + +// Validation class as described at +// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail +// decorator here because Arcjet handles this for you. +export class SignupDto { + @IsNotEmpty() + // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned + email: string; +} + +// This would normally go in your service file e.g. +// src/signup/signup.service.ts +@Injectable() +export class SignupService { + private readonly logger = new Logger(SignupService.name); + + signup(email: string): { message: string } { + this.logger.log(`Form submission: ${email}`); + + return { + message: "Hello world", + }; + } +} + +// This would normally go in your controller file e.g. +// src/signup/signup.controller.ts +@Controller("signup") +export class SignupController { + private readonly logger = new Logger(SignupController.name); + + constructor( + private readonly signupService: SignupService, + @Inject(ARCJET) private readonly arcjet: ArcjetNest, + ) {} + + // Implement a form handler following + // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't + // compatible with the NestJS Fastify adapter. + @Post() + @UseInterceptors(NoFilesInterceptor()) + async index(@Req() req: Request, @Body() body: SignupDto) { + const decision = await this.arcjet + .withRule( + validateEmail({ + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + // block disposable, invalid, and email addresses with no MX records + block: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], + }), + ) + .protect(req, { email: body.email }); + + this.logger.log(`Arcjet: id = ${decision.id}`); + this.logger.log(`Arcjet: decision = ${decision.conclusion}`); + + if (decision.isDenied()) { + if (decision.reason.isEmail()) { + this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`); + + let message: string; + + // These are specific errors to help the user, but will also reveal the + // validation to a spammer. + if (decision.reason.emailTypes.includes("INVALID")) { + message = "email address format is invalid. Is there a typo?"; + } else if (decision.reason.emailTypes.includes("DISPOSABLE")) { + message = "we do not allow disposable email addresses."; + } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) { + message = + "your email domain does not have an MX record. Is there a typo?"; + } else { + // This is a catch all, but the above should be exhaustive based on the + // configured rules. + message = "invalid email."; + } + + throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST); + } else { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); + } + } else if (decision.isErrored()) { + if (decision.reason.message.includes("missing User-Agent header")) { + // Requests without User-Agent headers can not be identified as any + // particular bot and will be marked as an errored decision. Most + // legitimate clients always send this header, so we recommend blocking + // requests without it. + this.logger.warn("User-Agent header is missing"); + throw new HttpException("Bad request", HttpStatus.BAD_REQUEST); + } else { + // 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: ${decision.reason.message}`); + } + } + + return this.signupService.signup(body.email); + } +} diff --git a/src/snippets/get-started/nest-js/Step3.mdx b/src/snippets/get-started/nest-js/Step3.mdx index 04d43675..ae72e18c 100644 --- a/src/snippets/get-started/nest-js/Step3.mdx +++ b/src/snippets/get-started/nest-js/Step3.mdx @@ -7,12 +7,13 @@ import GlobalGuard from "./GlobalGuard.ts?raw"; Update your `src/main.ts` file with the contents: {" "} + -This creates a global guard that will be applied to all routes. In a real -application, implementing guards or per-route protections would give you more -flexibility. See [our example app](https://github.com/arcjet/example-nestjs) for -how to do this. +This creates a global [guard](https://docs.nestjs.com/guards) that will be +applied to all routes. In a real application, implementing guards or per-route +protections would give you more flexibility. See [our example +app](https://github.com/arcjet/example-nestjs) for how to do this.
diff --git a/src/snippets/rate-limiting/reference/nestjs/DecoratorRoutes.mdx b/src/snippets/rate-limiting/reference/nestjs/DecoratorRoutes.mdx index 4780d11a..91ffdf52 100644 --- a/src/snippets/rate-limiting/reference/nestjs/DecoratorRoutes.mdx +++ b/src/snippets/rate-limiting/reference/nestjs/DecoratorRoutes.mdx @@ -7,7 +7,9 @@ import WithinRoute from "./WithinRoute.ts?raw"; ## Guards and routes -Arcjet can be integrated into NestJS in several places: +Arcjet can be integrated into NestJS in several places using NestJS +[guards](https://docs.nestjs.com/guards) or directly within the route +controller: - **Global guard:** Applies Arcjet rules on every request, but does not allow you to configure rules per route. diff --git a/src/snippets/reference/nestjs/Configuration.ts b/src/snippets/reference/nestjs/Configuration.ts index e6380a47..13f8258e 100644 --- a/src/snippets/reference/nestjs/Configuration.ts +++ b/src/snippets/reference/nestjs/Configuration.ts @@ -1,4 +1,4 @@ -import { ArcjetModule, fixedWindow } from "@arcjet/nest"; +import { ArcjetModule } from "@arcjet/nest"; import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; //import { AppController } from './app.controller.js'; diff --git a/src/snippets/sensitive-info/quick-start/bun/Step3.js b/src/snippets/sensitive-info/quick-start/bun/Step3.js index 11848a0e..bf3fdeb3 100644 --- a/src/snippets/sensitive-info/quick-start/bun/Step3.js +++ b/src/snippets/sensitive-info/quick-start/bun/Step3.js @@ -4,10 +4,11 @@ import { env } from "bun"; const aj = arcjet({ key: env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ - // Configured to return a deny if an email is detected + // This allows all sensitive entities other than email addresses sensitiveInfo({ mode: "LIVE", // Will block requests, use "DRY_RUN" to log only - deny: ["EMAIL"], + // allow: ["EMAIL"], Will block all sensitive information types other than email. + deny: ["EMAIL"], // Will block email addresses }), ], }); diff --git a/src/snippets/sensitive-info/quick-start/bun/Step3.ts b/src/snippets/sensitive-info/quick-start/bun/Step3.ts index 9484b8b8..acf32cf7 100644 --- a/src/snippets/sensitive-info/quick-start/bun/Step3.ts +++ b/src/snippets/sensitive-info/quick-start/bun/Step3.ts @@ -4,10 +4,11 @@ import { env } from "bun"; const aj = arcjet({ key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ - // Configured to return a deny if an email is detected + // This allows all sensitive entities other than email addresses sensitiveInfo({ mode: "LIVE", // Will block requests, use "DRY_RUN" to log only - deny: ["EMAIL"], + // allow: ["EMAIL"], Will block all sensitive information types other than email. + deny: ["EMAIL"], // Will block email addresses }), ], }); diff --git a/src/snippets/sensitive-info/quick-start/nestjs/Step1.mdx b/src/snippets/sensitive-info/quick-start/nestjs/Step1.mdx new file mode 100644 index 00000000..070694fd --- /dev/null +++ b/src/snippets/sensitive-info/quick-start/nestjs/Step1.mdx @@ -0,0 +1,26 @@ +import SelectableContent from "@/components/SelectableContent"; + +{/* prettier-ignore */} + +
+ +```sh +npm i @arcjet/nest +``` + +
+
+ +```sh +pnpm add @arcjet/nest +``` + +
+
+ +```sh +yarn add @arcjet/nest +``` + +
+
diff --git a/src/snippets/sensitive-info/quick-start/nestjs/Step3.js b/src/snippets/sensitive-info/quick-start/nestjs/Step3.js new file mode 100644 index 00000000..4c26ed09 --- /dev/null +++ b/src/snippets/sensitive-info/quick-start/nestjs/Step3.js @@ -0,0 +1,38 @@ +import { ArcjetGuard, ArcjetModule, sensitiveInfo } from "@arcjet/nest"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { APP_GUARD, NestFactory } from "@nestjs/core"; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + 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 + }), + ], + }), + ], + controllers: [], + providers: [ + { + provide: APP_GUARD, + useClass: ArcjetGuard, + }, + ], +}) +class AppModule {} + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); +} +bootstrap(); diff --git a/src/snippets/sensitive-info/quick-start/nestjs/Step3.mdx b/src/snippets/sensitive-info/quick-start/nestjs/Step3.mdx new file mode 100644 index 00000000..5d374eb0 --- /dev/null +++ b/src/snippets/sensitive-info/quick-start/nestjs/Step3.mdx @@ -0,0 +1,23 @@ +import Step3TS from "./Step3.ts?raw"; +import Step3JS from "./Step3.js?raw"; +import { Code } from "@astrojs/starlight/components"; +import SelectableContent from "@/components/SelectableContent"; + +This creates a global [guard](https://docs.nestjs.com/guards) that will be +applied to all routes. In a real application, implementing guards or per-route +protections would give you more flexibility. See the [reference +guide](/sensitive-info/reference) and the [NestJS example +app](https://github.com/arcjet/example-nestjs) for how to do this. + + +
+ + + +
+
+ + + +
+
diff --git a/src/snippets/sensitive-info/quick-start/nestjs/Step3.ts b/src/snippets/sensitive-info/quick-start/nestjs/Step3.ts new file mode 100644 index 00000000..977d6c1b --- /dev/null +++ b/src/snippets/sensitive-info/quick-start/nestjs/Step3.ts @@ -0,0 +1,38 @@ +import { ArcjetGuard, ArcjetModule, sensitiveInfo } from "@arcjet/nest"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { APP_GUARD, NestFactory } from "@nestjs/core"; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + 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 + }), + ], + }), + ], + controllers: [], + providers: [ + { + provide: APP_GUARD, + useClass: ArcjetGuard, + }, + ], +}) +class AppModule {} + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); +} +bootstrap(); diff --git a/src/snippets/sensitive-info/quick-start/nestjs/Step4.mdx b/src/snippets/sensitive-info/quick-start/nestjs/Step4.mdx new file mode 100644 index 00000000..e34b6408 --- /dev/null +++ b/src/snippets/sensitive-info/quick-start/nestjs/Step4.mdx @@ -0,0 +1,3 @@ +```bash +curl -v http://localhost:3000 --data "My email address is test@example.com" +``` diff --git a/src/snippets/sensitive-info/quick-start/nextjs/Step3App.js b/src/snippets/sensitive-info/quick-start/nextjs/Step3App.js index 039a8f58..385d256a 100644 --- a/src/snippets/sensitive-info/quick-start/nextjs/Step3App.js +++ b/src/snippets/sensitive-info/quick-start/nextjs/Step3App.js @@ -4,7 +4,7 @@ import { NextResponse } from "next/server"; const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ - // This allows all sensitive entities other than email addresses and those containing a dash character. + // 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. diff --git a/src/snippets/sensitive-info/quick-start/nextjs/Step3App.ts b/src/snippets/sensitive-info/quick-start/nextjs/Step3App.ts index a7c85518..22077ba4 100644 --- a/src/snippets/sensitive-info/quick-start/nextjs/Step3App.ts +++ b/src/snippets/sensitive-info/quick-start/nextjs/Step3App.ts @@ -4,7 +4,7 @@ import { NextResponse } from "next/server"; const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ - // This allows all sensitive entities other than email addresses and those containing a dash character. + // 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. diff --git a/src/snippets/sensitive-info/quick-start/nextjs/Step3Pages.js b/src/snippets/sensitive-info/quick-start/nextjs/Step3Pages.js index 39f4bb90..30dd2392 100644 --- a/src/snippets/sensitive-info/quick-start/nextjs/Step3Pages.js +++ b/src/snippets/sensitive-info/quick-start/nextjs/Step3Pages.js @@ -3,7 +3,7 @@ import arcjet, { sensitiveInfo } from "@arcjet/next"; const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ - // This allows all sensitive entities other than email addresses and those containing a dash character. + // 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. diff --git a/src/snippets/sensitive-info/quick-start/nextjs/Step3Pages.ts b/src/snippets/sensitive-info/quick-start/nextjs/Step3Pages.ts index 4b208a27..c8a92562 100644 --- a/src/snippets/sensitive-info/quick-start/nextjs/Step3Pages.ts +++ b/src/snippets/sensitive-info/quick-start/nextjs/Step3Pages.ts @@ -4,7 +4,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ - // This allows all sensitive entities other than email addresses and those containing a dash character. + // 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. diff --git a/src/snippets/sensitive-info/quick-start/nodejs/Step3.js b/src/snippets/sensitive-info/quick-start/nodejs/Step3.js index 64ae4a2d..07d9d3fa 100644 --- a/src/snippets/sensitive-info/quick-start/nodejs/Step3.js +++ b/src/snippets/sensitive-info/quick-start/nodejs/Step3.js @@ -6,7 +6,7 @@ const aj = arcjet({ // and set it as an environment variable rather than hard coding. key: process.env.ARCJET_KEY, rules: [ - // This allows all sensitive entities other than email addresses and those containing a dash character. + // 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. diff --git a/src/snippets/sensitive-info/quick-start/nodejs/Step3.ts b/src/snippets/sensitive-info/quick-start/nodejs/Step3.ts index 2d469ebb..a6af1ec4 100644 --- a/src/snippets/sensitive-info/quick-start/nodejs/Step3.ts +++ b/src/snippets/sensitive-info/quick-start/nodejs/Step3.ts @@ -6,7 +6,7 @@ const aj = arcjet({ // and set it as an environment variable rather than hard coding. key: process.env.ARCJET_KEY!, rules: [ - // This allows all sensitive entities other than email addresses and those containing a dash character. + // 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. diff --git a/src/snippets/sensitive-info/quick-start/remix/Step3.jsx b/src/snippets/sensitive-info/quick-start/remix/Step3.jsx index 23db8450..a80415f3 100644 --- a/src/snippets/sensitive-info/quick-start/remix/Step3.jsx +++ b/src/snippets/sensitive-info/quick-start/remix/Step3.jsx @@ -5,7 +5,7 @@ const aj = arcjet({ // and set it as an environment variable rather than hard coding. key: process.env.ARCJET_KEY, rules: [ - // This allows all sensitive entities other than email addresses and those containing a dash character. + // 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. diff --git a/src/snippets/sensitive-info/quick-start/remix/Step3.tsx b/src/snippets/sensitive-info/quick-start/remix/Step3.tsx index 9ac42904..f782db87 100644 --- a/src/snippets/sensitive-info/quick-start/remix/Step3.tsx +++ b/src/snippets/sensitive-info/quick-start/remix/Step3.tsx @@ -6,7 +6,7 @@ const aj = arcjet({ // and set it as an environment variable rather than hard coding. key: process.env.ARCJET_KEY!, rules: [ - // This allows all sensitive entities other than email addresses and those containing a dash character. + // 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. diff --git a/src/snippets/sensitive-info/quick-start/sveltekit/Step3.js b/src/snippets/sensitive-info/quick-start/sveltekit/Step3.js index a9554148..07a18c73 100644 --- a/src/snippets/sensitive-info/quick-start/sveltekit/Step3.js +++ b/src/snippets/sensitive-info/quick-start/sveltekit/Step3.js @@ -5,7 +5,7 @@ import { error } from "@sveltejs/kit"; const aj = arcjet({ key: env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ - // This allows all sensitive entities other than email addresses and those containing a dash character. + // 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. diff --git a/src/snippets/sensitive-info/quick-start/sveltekit/Step3.ts b/src/snippets/sensitive-info/quick-start/sveltekit/Step3.ts index 2497effd..d7dca84a 100644 --- a/src/snippets/sensitive-info/quick-start/sveltekit/Step3.ts +++ b/src/snippets/sensitive-info/quick-start/sveltekit/Step3.ts @@ -5,7 +5,7 @@ import { error, type RequestEvent } from "@sveltejs/kit"; const aj = arcjet({ key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ - // This allows all sensitive entities other than email addresses and those containing a dash character. + // 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. diff --git a/src/snippets/sensitive-info/reference/nestjs/CustomDetect.mdx b/src/snippets/sensitive-info/reference/nestjs/CustomDetect.mdx new file mode 100644 index 00000000..40a3d7cc --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/CustomDetect.mdx @@ -0,0 +1,9 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import CustomDetectTS from "./CustomDetect.ts?raw"; + + +
+ +
+
diff --git a/src/snippets/sensitive-info/reference/nestjs/CustomDetect.ts b/src/snippets/sensitive-info/reference/nestjs/CustomDetect.ts new file mode 100644 index 00000000..e7858dfa --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/CustomDetect.ts @@ -0,0 +1,86 @@ +import { ARCJET, type ArcjetNest, sensitiveInfo } from "@arcjet/nest"; +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 e.g. +// 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 e.g. +// src/page/page.controller.ts +// This function is called by the`sensitiveInfo` rule to perform custom +// detection on strings. +function detectDash(tokens: string[]): Array<"CONTAINS_DASH" | undefined> { + return tokens.map((token) => { + if (token.includes("-")) { + return "CONTAINS_DASH"; + } + }); +} + +@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", "CONTAINS_DASH"], // Will block email addresses and strings containing a dash + detect: detectDash, + contextWindowSize: 2, + }), + ) + .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); + } +} diff --git a/src/snippets/sensitive-info/reference/nestjs/DecisionLog.mdx b/src/snippets/sensitive-info/reference/nestjs/DecisionLog.mdx new file mode 100644 index 00000000..c4979e93 --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/DecisionLog.mdx @@ -0,0 +1,9 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import DecisionLogTS from "./DecisionLog.ts?raw"; + + +
+ +
+
diff --git a/src/snippets/sensitive-info/reference/nestjs/DecisionLog.ts b/src/snippets/sensitive-info/reference/nestjs/DecisionLog.ts new file mode 100644 index 00000000..91f4590d --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/DecisionLog.ts @@ -0,0 +1,101 @@ +import { + ARCJET, + type ArcjetNest, + detectBot, + sensitiveInfo, +} from "@arcjet/nest"; +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 e.g. +// 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 e.g. +// 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 + }), + ) + .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); + } + } + + return this.pageService.message(body); + } +} diff --git a/src/snippets/sensitive-info/reference/nestjs/DecoratorRoutes.mdx b/src/snippets/sensitive-info/reference/nestjs/DecoratorRoutes.mdx new file mode 100644 index 00000000..91ffdf52 --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/DecoratorRoutes.mdx @@ -0,0 +1,62 @@ +import { Code } from "@astrojs/starlight/components"; +import SelectableContent from "@/components/SelectableContent"; +import GlobalGuardTS from "./GlobalGuard.ts?raw"; +import GlobalGuardRouteTS from "./GlobalGuardRoute.ts?raw"; +import PerRouteGuard from "./PerRouteGuard.ts?raw"; +import WithinRoute from "./WithinRoute.ts?raw"; + +## Guards and routes + +Arcjet can be integrated into NestJS in several places using NestJS +[guards](https://docs.nestjs.com/guards) or directly within the route +controller: + +- **Global guard:** Applies Arcjet rules on every request, but does not allow + you to configure rules per route. +- **Per route guard:** Allows you to 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`. + + +
+ +
+
+ +This can then be added to the controller for all the routes you wish to protect +with Arcjet. + + +
+ +
+
+ +### 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. + +The rules will be applied and a generic error returned if the result is `DENY`. + + +
+ +
+
+ +### Within route + +Call Arcjet from within the route controller to have maximum flexibility. + + +
+ +
+
diff --git a/src/snippets/sensitive-info/reference/nestjs/Errors.mdx b/src/snippets/sensitive-info/reference/nestjs/Errors.mdx new file mode 100644 index 00000000..1ee27666 --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/Errors.mdx @@ -0,0 +1,9 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import ErrorsTS from "./Errors.ts?raw"; + + +
+ +
+
diff --git a/src/snippets/sensitive-info/reference/nestjs/Errors.ts b/src/snippets/sensitive-info/reference/nestjs/Errors.ts new file mode 100644 index 00000000..400b6c53 --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/Errors.ts @@ -0,0 +1,87 @@ +import { ARCJET, type ArcjetNest, sensitiveInfo } from "@arcjet/nest"; +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 e.g. +// 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 e.g. +// 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 + }), + ) + .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); + } + } else if (decision.isErrored()) { + if (decision.reason.message.includes("missing User-Agent header")) { + // Requests without User-Agent headers can not be identified as any + // particular bot and will be marked as an errored decision. Most + // legitimate clients always send this header, so we recommend blocking + // requests without it. + this.logger.warn("User-Agent header is missing"); + throw new HttpException("Bad request", HttpStatus.BAD_REQUEST); + } else { + // 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: ${decision.reason.message}`); + } + } + + return this.pageService.message(body); + } +} diff --git a/src/snippets/sensitive-info/reference/nestjs/GlobalGuard.ts b/src/snippets/sensitive-info/reference/nestjs/GlobalGuard.ts new file mode 100644 index 00000000..c5b728d6 --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/GlobalGuard.ts @@ -0,0 +1,30 @@ +import { ArcjetModule, sensitiveInfo } from "@arcjet/nest"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +//import { AppController } from './app.controller.js'; +//import { AppService } from './app.service.js'; + +@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 + }), + ], + }), + // ... other modules + ], + //controllers: [AppController], + //providers: [AppService], +}) +export class AppModule {} diff --git a/src/snippets/sensitive-info/reference/nestjs/GlobalGuardRoute.ts b/src/snippets/sensitive-info/reference/nestjs/GlobalGuardRoute.ts new file mode 100644 index 00000000..b0353988 --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/GlobalGuardRoute.ts @@ -0,0 +1,37 @@ +import { ArcjetGuard } from "@arcjet/nest"; +import { + Body, + Controller, + Injectable, + Post, + Req, + UseGuards, +} from "@nestjs/common"; + +// This would normally go in your controller file e.g. +// 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 e.g. +// src/page/page.service.ts +@Injectable() +export class PageService { + message(content: string): { message: string; submittedContent: string } { + return { + message: "Hello world", + submittedContent: content, + }; + } +} diff --git a/src/snippets/sensitive-info/reference/nestjs/PerRouteGuard.ts b/src/snippets/sensitive-info/reference/nestjs/PerRouteGuard.ts new file mode 100644 index 00000000..c978da28 --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/PerRouteGuard.ts @@ -0,0 +1,35 @@ +import { WithArcjetRules, sensitiveInfo } from "@arcjet/nest"; +import { Body, Injectable, Post, Req } from "@nestjs/common"; + +// This would normally go in your controller file e.g. +// 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 + }), +]) +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 e.g. +// src/page/page.service.ts +@Injectable() +export class PageService { + message(content: string): { message: string; submittedContent: string } { + return { + message: "Hello world", + submittedContent: content, + }; + } +} diff --git a/src/snippets/sensitive-info/reference/nestjs/WithinRoute.ts b/src/snippets/sensitive-info/reference/nestjs/WithinRoute.ts new file mode 100644 index 00000000..ebf4c544 --- /dev/null +++ b/src/snippets/sensitive-info/reference/nestjs/WithinRoute.ts @@ -0,0 +1,60 @@ +import { ARCJET, type ArcjetNest, sensitiveInfo } from "@arcjet/nest"; +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 e.g. +// 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 e.g. +// 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 + }), + ) + .protect(req); + + if (decision.isDenied()) { + if (decision.reason.isBot()) { + throw new HttpException("No bots allowed", HttpStatus.FORBIDDEN); + } else { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); + } + } + + return this.pageService.message(body); + } +} diff --git a/src/snippets/shield/quick-start/bun/Step3.js b/src/snippets/shield/quick-start/bun/Step3.js index 9fc4b909..d85fd269 100644 --- a/src/snippets/shield/quick-start/bun/Step3.js +++ b/src/snippets/shield/quick-start/bun/Step3.js @@ -5,6 +5,7 @@ const aj = arcjet({ key: env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ // Shield protects your app from common attacks like SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ mode: "LIVE" }), ], }); diff --git a/src/snippets/shield/quick-start/bun/Step3.ts b/src/snippets/shield/quick-start/bun/Step3.ts index 2fbc7354..0febd149 100644 --- a/src/snippets/shield/quick-start/bun/Step3.ts +++ b/src/snippets/shield/quick-start/bun/Step3.ts @@ -5,6 +5,7 @@ const aj = arcjet({ key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ // Shield protects your app from common attacks like SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ mode: "LIVE" }), ], }); diff --git a/src/snippets/shield/quick-start/nestjs/Step1.mdx b/src/snippets/shield/quick-start/nestjs/Step1.mdx new file mode 100644 index 00000000..b6a9d0ac --- /dev/null +++ b/src/snippets/shield/quick-start/nestjs/Step1.mdx @@ -0,0 +1,26 @@ +import SelectableContent from "@/components/SelectableContent"; + +{/* prettier-ignore */} + +
+ + ```sh + npm i @arcjet/nest + ``` +
+ +
+ +```sh +pnpm add @arcjet/nest +``` + +
+
+ +```sh +yarn add @arcjet/nest +``` + +
+
diff --git a/src/snippets/shield/quick-start/nestjs/Step3.js b/src/snippets/shield/quick-start/nestjs/Step3.js new file mode 100644 index 00000000..6a71b152 --- /dev/null +++ b/src/snippets/shield/quick-start/nestjs/Step3.js @@ -0,0 +1,35 @@ +import { ArcjetGuard, ArcjetModule, shield } from "@arcjet/nest"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { APP_GUARD, NestFactory } from "@nestjs/core"; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + ArcjetModule.forRoot({ + isGlobal: true, + key: process.env.ARCJET_KEY, + rules: [ + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block + shield({ mode: "DRY_RUN" }), + ], + }), + ], + controllers: [], + providers: [ + { + provide: APP_GUARD, + useClass: ArcjetGuard, + }, + ], +}) +class AppModule {} + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); +} +bootstrap(); diff --git a/src/snippets/shield/quick-start/nestjs/Step3.mdx b/src/snippets/shield/quick-start/nestjs/Step3.mdx new file mode 100644 index 00000000..128546a5 --- /dev/null +++ b/src/snippets/shield/quick-start/nestjs/Step3.mdx @@ -0,0 +1,19 @@ +import Step3TS from "./Step3.ts?raw"; +import Step3JS from "./Step3.js?raw"; +import { Code } from "@astrojs/starlight/components"; +import SelectableContent from "@/components/SelectableContent"; + +This creates a global [guard](https://docs.nestjs.com/guards) that will be +applied to all routes. In a real application, implementing guards or per-route +protections would give you more flexibility. See the [reference +guide](/shield/reference) and the [NestJS example +app](https://github.com/arcjet/example-nestjs) for how to do this. + + +
+ +
+
+ +
+
diff --git a/src/snippets/shield/quick-start/nestjs/Step3.ts b/src/snippets/shield/quick-start/nestjs/Step3.ts new file mode 100644 index 00000000..6d624483 --- /dev/null +++ b/src/snippets/shield/quick-start/nestjs/Step3.ts @@ -0,0 +1,35 @@ +import { ArcjetGuard, ArcjetModule, shield } from "@arcjet/nest"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { APP_GUARD, NestFactory } from "@nestjs/core"; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + ArcjetModule.forRoot({ + isGlobal: true, + key: process.env.ARCJET_KEY!, + rules: [ + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block + shield({ mode: "DRY_RUN" }), + ], + }), + ], + controllers: [], + providers: [ + { + provide: APP_GUARD, + useClass: ArcjetGuard, + }, + ], +}) +class AppModule {} + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); +} +bootstrap(); diff --git a/src/snippets/shield/quick-start/nestjs/Step4.mdx b/src/snippets/shield/quick-start/nestjs/Step4.mdx new file mode 100644 index 00000000..03e1460e --- /dev/null +++ b/src/snippets/shield/quick-start/nestjs/Step4.mdx @@ -0,0 +1,23 @@ +import { Aside } from "@astrojs/starlight/components"; +import SelectableContent from "@/components/SelectableContent"; + +### 4. Start app + +{/* prettier-ignore */} + +
+```sh +npm run start +``` +
+
+```sh +pnpm run start +``` +
+
+```sh +yarn run start +``` +
+
diff --git a/src/snippets/shield/quick-start/nestjs/Step5.mdx b/src/snippets/shield/quick-start/nestjs/Step5.mdx new file mode 100644 index 00000000..80f93c94 --- /dev/null +++ b/src/snippets/shield/quick-start/nestjs/Step5.mdx @@ -0,0 +1,19 @@ +```sh +curl -v -H "x-arcjet-suspicious: true" http://localhost:3000 +``` + +After the 5th request, you will see this in your logs: + +```text +Rule Result ArcjetRuleResult { + ttl: 0, + state: 'DRY_RUN', + conclusion: 'DENY', + reason: ArcjetShieldReason { type: 'SHIELD', shieldTriggered: true } +} +Conclusion ALLOW +``` + +The final conclusion is `ALLOW` even though the rule result conclusion is +`DENY`. This is because the rule is in dry run mode. Switch it to `LIVE` mode to +actually block the request. diff --git a/src/snippets/shield/quick-start/nextjs/PerRouteApp.js b/src/snippets/shield/quick-start/nextjs/PerRouteApp.js index f1e607aa..3bffc9a9 100644 --- a/src/snippets/shield/quick-start/nextjs/PerRouteApp.js +++ b/src/snippets/shield/quick-start/nextjs/PerRouteApp.js @@ -4,6 +4,8 @@ import { NextResponse } from "next/server"; const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ mode: "DRY_RUN", }), diff --git a/src/snippets/shield/quick-start/nextjs/PerRouteApp.ts b/src/snippets/shield/quick-start/nextjs/PerRouteApp.ts index 69956a72..940744e2 100644 --- a/src/snippets/shield/quick-start/nextjs/PerRouteApp.ts +++ b/src/snippets/shield/quick-start/nextjs/PerRouteApp.ts @@ -4,6 +4,8 @@ import { NextResponse } from "next/server"; const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ mode: "DRY_RUN", }), diff --git a/src/snippets/shield/quick-start/nextjs/PerRoutePages.js b/src/snippets/shield/quick-start/nextjs/PerRoutePages.js index 59f41e77..331d10a6 100644 --- a/src/snippets/shield/quick-start/nextjs/PerRoutePages.js +++ b/src/snippets/shield/quick-start/nextjs/PerRoutePages.js @@ -3,6 +3,8 @@ import arcjet, { shield } from "@arcjet/next"; const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ mode: "DRY_RUN", }), diff --git a/src/snippets/shield/quick-start/nextjs/PerRoutePages.ts b/src/snippets/shield/quick-start/nextjs/PerRoutePages.ts index fe2689be..2807016a 100644 --- a/src/snippets/shield/quick-start/nextjs/PerRoutePages.ts +++ b/src/snippets/shield/quick-start/nextjs/PerRoutePages.ts @@ -4,6 +4,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ mode: "DRY_RUN", }), diff --git a/src/snippets/shield/quick-start/nodejs/Step3.js b/src/snippets/shield/quick-start/nodejs/Step3.js index ab99d2c5..31092c52 100644 --- a/src/snippets/shield/quick-start/nodejs/Step3.js +++ b/src/snippets/shield/quick-start/nodejs/Step3.js @@ -6,9 +6,10 @@ const aj = arcjet({ // and set it as an environment variable rather than hard coding. key: process.env.ARCJET_KEY, rules: [ - // Protect against common attacks with Arcjet Shield + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ - mode: "DRY_RUN", // Change to "LIVE" to block requests + mode: "DRY_RUN", }), ], }); diff --git a/src/snippets/shield/quick-start/nodejs/Step3.ts b/src/snippets/shield/quick-start/nodejs/Step3.ts index 71d23320..1cabf260 100644 --- a/src/snippets/shield/quick-start/nodejs/Step3.ts +++ b/src/snippets/shield/quick-start/nodejs/Step3.ts @@ -6,9 +6,10 @@ const aj = arcjet({ // and set it as an environment variable rather than hard coding. key: process.env.ARCJET_KEY!, rules: [ - // Protect against common attacks with Arcjet Shield + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ - mode: "DRY_RUN", // Change to "LIVE" to block requests + mode: "DRY_RUN", }), ], }); diff --git a/src/snippets/shield/quick-start/remix/Step3.jsx b/src/snippets/shield/quick-start/remix/Step3.jsx index 77de90c1..ed60d97a 100644 --- a/src/snippets/shield/quick-start/remix/Step3.jsx +++ b/src/snippets/shield/quick-start/remix/Step3.jsx @@ -5,9 +5,10 @@ const aj = arcjet({ // and set it as an environment variable rather than hard coding. key: process.env.ARCJET_KEY, rules: [ - // Protect against common attacks with Arcjet Shield + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ - mode: "DRY_RUN", // will block requests. Use "DRY_RUN" to log only + mode: "DRY_RUN", }), ], }); diff --git a/src/snippets/shield/quick-start/remix/Step3.tsx b/src/snippets/shield/quick-start/remix/Step3.tsx index 121c87b2..6361c295 100644 --- a/src/snippets/shield/quick-start/remix/Step3.tsx +++ b/src/snippets/shield/quick-start/remix/Step3.tsx @@ -6,9 +6,10 @@ const aj = arcjet({ // and set it as an environment variable rather than hard coding. key: process.env.ARCJET_KEY!, rules: [ - // Protect against common attacks with Arcjet Shield + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ - mode: "DRY_RUN", // will block requests. Use "DRY_RUN" to log only + mode: "DRY_RUN", }), ], }); diff --git a/src/snippets/shield/quick-start/sveltekit/Step3.js b/src/snippets/shield/quick-start/sveltekit/Step3.js index 65a50c2e..38281654 100644 --- a/src/snippets/shield/quick-start/sveltekit/Step3.js +++ b/src/snippets/shield/quick-start/sveltekit/Step3.js @@ -5,9 +5,10 @@ import { error } from "@sveltejs/kit"; const aj = arcjet({ key: env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ - // Protect against common attacks with Arcjet Shield + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ - mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + mode: "LIVE", }), ], }); diff --git a/src/snippets/shield/quick-start/sveltekit/Step3.ts b/src/snippets/shield/quick-start/sveltekit/Step3.ts index 279dd324..eeb75644 100644 --- a/src/snippets/shield/quick-start/sveltekit/Step3.ts +++ b/src/snippets/shield/quick-start/sveltekit/Step3.ts @@ -5,9 +5,10 @@ import { error, type RequestEvent } from "@sveltejs/kit"; const aj = arcjet({ key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ - // Protect against common attacks with Arcjet Shield + // Shield protects your app from common attacks e.g. SQL injection + // DRY_RUN mode logs only. Use "LIVE" to block shield({ - mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + mode: "LIVE", }), ], }); diff --git a/src/snippets/shield/reference/nestjs/DecisionLog.mdx b/src/snippets/shield/reference/nestjs/DecisionLog.mdx new file mode 100644 index 00000000..c4979e93 --- /dev/null +++ b/src/snippets/shield/reference/nestjs/DecisionLog.mdx @@ -0,0 +1,9 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import DecisionLogTS from "./DecisionLog.ts?raw"; + + +
+ +
+
diff --git a/src/snippets/shield/reference/nestjs/DecisionLog.ts b/src/snippets/shield/reference/nestjs/DecisionLog.ts new file mode 100644 index 00000000..3f54fdfe --- /dev/null +++ b/src/snippets/shield/reference/nestjs/DecisionLog.ts @@ -0,0 +1,86 @@ +import { ARCJET, type ArcjetNest, detectBot, shield } from "@arcjet/nest"; +import { + Controller, + Get, + HttpException, + HttpStatus, + Inject, + Injectable, + Logger, + Req, +} from "@nestjs/common"; +import type { Request } from "express"; + +// This would normally go in your service file e.g. +// src/page/page.service.ts +@Injectable() +export class PageService { + message(): { message: string } { + return { + message: "Hello world", + }; + } +} + +// This would normally go in your controller file e.g. +// 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, + ) {} + + @Get() + async index(@Req() req: Request) { + 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( + shield({ + mode: "LIVE", + }), + ) + .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.isShield()) { + this.logger.log("Shield 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 { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); + } + } + + return this.pageService.message(); + } +} diff --git a/src/snippets/shield/reference/nestjs/DecoratorRoutes.mdx b/src/snippets/shield/reference/nestjs/DecoratorRoutes.mdx new file mode 100644 index 00000000..baa76f70 --- /dev/null +++ b/src/snippets/shield/reference/nestjs/DecoratorRoutes.mdx @@ -0,0 +1,65 @@ +import { Code } from "@astrojs/starlight/components"; +import SelectableContent from "@/components/SelectableContent"; +import GlobalGuardTS from "./GlobalGuard.ts?raw"; +import GlobalGuardRouteTS from "./GlobalGuardRoute.ts?raw"; +import PerRouteGuard from "./PerRouteGuard.ts?raw"; +import WithinRoute from "./WithinRoute.ts?raw"; + +## Guards and routes + +Arcjet can be integrated into NestJS in several places using NestJS +[guards](https://docs.nestjs.com/guards) or directly within the route +controller: + +- **Global guard:** Applies Arcjet rules on every request, but does not allow + you to configure rules per route. +- **Per route guard:** Allows you to 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. + +For Shield we recommend including it on the root module so it can analyze all +requests. + +### Global guard + +A global guard can be configured in `src/app.module.ts`. + + +
+ +
+
+ +This can then be added to the controller for all the routes you wish to protect +with Arcjet. + + +
+ +
+
+ +### 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. + +The rules will be applied and a generic error returned if the result is `DENY`. + + +
+ +
+
+ +### Within route + +Call Arcjet from within the route controller to have maximum flexibility. + + +
+ +
+
diff --git a/src/snippets/shield/reference/nestjs/Errors.mdx b/src/snippets/shield/reference/nestjs/Errors.mdx new file mode 100644 index 00000000..cf71aa34 --- /dev/null +++ b/src/snippets/shield/reference/nestjs/Errors.mdx @@ -0,0 +1,13 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import ErrorsJS from "./Errors.js?raw"; +import ErrorsTS from "./Errors.ts?raw"; + + +
+ +
+
+ +
+
diff --git a/src/snippets/shield/reference/nestjs/Errors.ts b/src/snippets/shield/reference/nestjs/Errors.ts new file mode 100644 index 00000000..8ff7c2b2 --- /dev/null +++ b/src/snippets/shield/reference/nestjs/Errors.ts @@ -0,0 +1,72 @@ +import { ARCJET, type ArcjetNest, shield } from "@arcjet/nest"; +import { + Controller, + Get, + HttpException, + HttpStatus, + Inject, + Injectable, + Logger, + Req, +} from "@nestjs/common"; +import type { Request } from "express"; + +// This would normally go in your service file e.g. +// src/page/page.service.ts +@Injectable() +export class PageService { + message(): { message: string } { + return { + message: "Hello world", + }; + } +} + +// This would normally go in your controller file e.g. +// 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, + ) {} + + @Get() + async index(@Req() req: Request) { + const decision = await this.arcjet + .withRule( + shield({ + mode: "LIVE", + }), + ) + .protect(req); + + if (decision.isDenied()) { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); + } else if (decision.isErrored()) { + if (decision.reason.message.includes("missing User-Agent header")) { + // Requests without User-Agent headers can not be identified as any + // particular bot and will be marked as an errored decision. Most + // legitimate clients always send this header, so we recommend blocking + // requests without it. + this.logger.warn("User-Agent header is missing"); + throw new HttpException("Bad request", HttpStatus.BAD_REQUEST); + } else { + // 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: ${decision.reason.message}`); + } + } + + return this.pageService.message(); + } +} diff --git a/src/snippets/shield/reference/nestjs/GlobalGuard.ts b/src/snippets/shield/reference/nestjs/GlobalGuard.ts new file mode 100644 index 00000000..e2a49f0e --- /dev/null +++ b/src/snippets/shield/reference/nestjs/GlobalGuard.ts @@ -0,0 +1,28 @@ +import { ArcjetModule, shield } from "@arcjet/nest"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +//import { AppController } from './app.controller.js'; +//import { AppService } from './app.service.js'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: ".env.local", + }), + ArcjetModule.forRoot({ + isGlobal: true, + key: process.env.ARCJET_KEY!, + rules: [ + // Applies to every request + shield({ + mode: "LIVE", + }), + ], + }), + // ... other modules + ], + //controllers: [AppController], + //providers: [AppService], +}) +export class AppModule {} diff --git a/src/snippets/shield/reference/nestjs/GlobalGuardRoute.ts b/src/snippets/shield/reference/nestjs/GlobalGuardRoute.ts new file mode 100644 index 00000000..2b0c72d8 --- /dev/null +++ b/src/snippets/shield/reference/nestjs/GlobalGuardRoute.ts @@ -0,0 +1,29 @@ +import { ArcjetGuard } from "@arcjet/nest"; +import { Controller, Get, Injectable, UseGuards } from "@nestjs/common"; + +// This would normally go in your controller file e.g. +// 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) {} + + @Get() + index() { + return this.pageService.message(); + } +} + +// This would normally go in your service file e.g. +// src/page/page.service.ts +@Injectable() +export class PageService { + message(): { message: string } { + return { + message: "Hello world", + }; + } +} diff --git a/src/snippets/shield/reference/nestjs/PerRouteGuard.ts b/src/snippets/shield/reference/nestjs/PerRouteGuard.ts new file mode 100644 index 00000000..1036ebf1 --- /dev/null +++ b/src/snippets/shield/reference/nestjs/PerRouteGuard.ts @@ -0,0 +1,31 @@ +import { WithArcjetRules, shield } from "@arcjet/nest"; +import { Injectable, Get } from "@nestjs/common"; + +// This would normally go in your controller file e.g. +// 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([ + shield({ + mode: "LIVE", + }), +]) +export class PageController { + constructor(private readonly pageService: PageService) {} + + @Get() + index() { + return this.pageService.message(); + } +} + +// This would normally go in your service file e.g. +// src/page/page.service.ts +@Injectable() +export class PageService { + message(): { message: string } { + return { + message: "Hello world", + }; + } +} diff --git a/src/snippets/shield/reference/nestjs/WithinRoute.ts b/src/snippets/shield/reference/nestjs/WithinRoute.ts new file mode 100644 index 00000000..b7fb793b --- /dev/null +++ b/src/snippets/shield/reference/nestjs/WithinRoute.ts @@ -0,0 +1,55 @@ +import { ARCJET, type ArcjetNest, shield } from "@arcjet/nest"; +import { + Controller, + Get, + HttpException, + HttpStatus, + Inject, + Injectable, + Req, +} from "@nestjs/common"; +import type { Request } from "express"; + +// This would normally go in your service file e.g. +// src/page/page.service.ts +@Injectable() +export class PageAdvancedService { + message(): { message: string } { + return { + message: "Hello world", + }; + } +} + +// This would normally go in your controller file e.g. +// 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 PageAdvancedController { + constructor( + private readonly pageService: PageAdvancedService, + @Inject(ARCJET) private readonly arcjet: ArcjetNest, + ) {} + + @Get() + async index(@Req() req: Request) { + const decision = await this.arcjet + .withRule( + shield({ + mode: "LIVE", + }), + ) + .protect(req); + + if (decision.isDenied()) { + if (decision.reason.isShield()) { + throw new HttpException("No attacks allowed", HttpStatus.FORBIDDEN); + } else { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); + } + } + + return this.pageService.message(); + } +} diff --git a/src/snippets/signup-protection/quick-start/nestjs/Step1.mdx b/src/snippets/signup-protection/quick-start/nestjs/Step1.mdx new file mode 100644 index 00000000..070694fd --- /dev/null +++ b/src/snippets/signup-protection/quick-start/nestjs/Step1.mdx @@ -0,0 +1,26 @@ +import SelectableContent from "@/components/SelectableContent"; + +{/* prettier-ignore */} + +
+ +```sh +npm i @arcjet/nest +``` + +
+
+ +```sh +pnpm add @arcjet/nest +``` + +
+
+ +```sh +yarn add @arcjet/nest +``` + +
+
diff --git a/src/snippets/signup-protection/quick-start/nestjs/Step3.mdx b/src/snippets/signup-protection/quick-start/nestjs/Step3.mdx new file mode 100644 index 00000000..d1adf78d --- /dev/null +++ b/src/snippets/signup-protection/quick-start/nestjs/Step3.mdx @@ -0,0 +1,21 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import Step3AppModuleTS from "./Step3AppModule.ts?raw"; +import Step3ControllerTS from "./Step3Controller.ts?raw"; + +Several files are combined here to demonstrate creating a form handler +controller. In a real application you should split them as suggested in the +comments. + + +
+ +
+
+ +
+
diff --git a/src/snippets/signup-protection/quick-start/nestjs/Step3AppModule.ts b/src/snippets/signup-protection/quick-start/nestjs/Step3AppModule.ts new file mode 100644 index 00000000..78809634 --- /dev/null +++ b/src/snippets/signup-protection/quick-start/nestjs/Step3AppModule.ts @@ -0,0 +1,34 @@ +import { ArcjetGuard, ArcjetModule } from "@arcjet/nest"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { APP_GUARD, NestFactory } from "@nestjs/core"; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + ArcjetModule.forRoot({ + isGlobal: true, + key: process.env.ARCJET_KEY!, + rules: [ + // We're not adding any rules here for this example, but if you did they + // would be the default rules wherever you use Arcjet. + ], + }), + ], + controllers: [], + providers: [ + { + provide: APP_GUARD, + useClass: ArcjetGuard, + }, + ], +}) +class AppModule {} + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); +} +bootstrap(); diff --git a/src/snippets/signup-protection/quick-start/nestjs/Step3Controller.ts b/src/snippets/signup-protection/quick-start/nestjs/Step3Controller.ts new file mode 100644 index 00000000..c4e1376c --- /dev/null +++ b/src/snippets/signup-protection/quick-start/nestjs/Step3Controller.ts @@ -0,0 +1,124 @@ +import { ARCJET, type ArcjetNest, protectSignup } from "@arcjet/nest"; +import { + Body, + Controller, + HttpException, + HttpStatus, + Inject, + Injectable, + Logger, + Post, + Req, + UseInterceptors, +} from "@nestjs/common"; +import { NoFilesInterceptor } from "@nestjs/platform-express"; +import { IsNotEmpty } from "class-validator"; +import type { Request } from "express"; + +// Validation class as described at +// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail +// decorator here because Arcjet handles this for you. +export class SignupDto { + @IsNotEmpty() + // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned + email: string; +} + +// This would normally go in your service file e.g. +// src/signup/signup.service.ts +@Injectable() +export class SignupService { + private readonly logger = new Logger(SignupService.name); + + signup(email: string): { message: string } { + this.logger.log(`Form submission: ${email}`); + + return { + message: "Hello world", + }; + } +} + +// This would normally go in your controller file e.g. +// src/signup/signup.controller.ts +@Controller("signup") +export class SignupController { + private readonly logger = new Logger(SignupController.name); + + constructor( + private readonly signupService: SignupService, + @Inject(ARCJET) private readonly arcjet: ArcjetNest, + ) {} + + // Implement a form handler following + // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't + // compatible with the NestJS Fastify adapter. + @Post() + @UseInterceptors(NoFilesInterceptor()) + async index(@Req() req: Request, @Body() body: SignupDto) { + const decision = await this.arcjet + .withRule( + protectSignup({ + email: { + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + // Block emails that are disposable, invalid, or have no MX records + block: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], + }, + bots: { + mode: "LIVE", + // configured with a list of bots to allow from + // https://arcjet.com/bot-list + allow: ["CURL"], // prevents bots from submitting the form, but allow curl for this example + }, + // It would be unusual for a form to be submitted more than 5 times in 10 + // minutes from the same IP address + rateLimit: { + // uses a sliding window rate limit + mode: "LIVE", + interval: "2m", // counts requests over a 10 minute sliding window + max: 5, // allows 5 submissions within the window + }, + }), + ) + .protect(req, { email: body.email }); + + this.logger.log(`Arcjet: id = ${decision.id}`); + this.logger.log(`Arcjet: decision = ${decision.conclusion}`); + + if (decision.isDenied()) { + if (decision.reason.isBot()) { + throw new HttpException("No bots allowed", HttpStatus.FORBIDDEN); + } else if (decision.reason.isRateLimit()) { + throw new HttpException( + "Too many requests", + HttpStatus.TOO_MANY_REQUESTS, + ); + } else if (decision.reason.isEmail()) { + this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`); + + let message: string; + + // These are specific errors to help the user, but will also reveal the + // validation to a spammer. + if (decision.reason.emailTypes.includes("INVALID")) { + message = "email address format is invalid. Is there a typo?"; + } else if (decision.reason.emailTypes.includes("DISPOSABLE")) { + message = "we do not allow disposable email addresses."; + } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) { + message = + "your email domain does not have an MX record. Is there a typo?"; + } else { + // This is a catch all, but the above should be exhaustive based on the + // configured rules. + message = "invalid email."; + } + + throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST); + } else { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); + } + } + + return this.signupService.signup(body.email); + } +} diff --git a/src/snippets/signup-protection/quick-start/nestjs/Step4.mdx b/src/snippets/signup-protection/quick-start/nestjs/Step4.mdx new file mode 100644 index 00000000..6f87a2eb --- /dev/null +++ b/src/snippets/signup-protection/quick-start/nestjs/Step4.mdx @@ -0,0 +1,30 @@ +import { Aside } from "@astrojs/starlight/components"; +import SelectableContent from "@/components/SelectableContent"; + +### 4. Start app + +{/* prettier-ignore */} + +
+```sh +npm run start +``` +
+
+```sh +pnpm run start +``` +
+
+```sh +yarn run start +``` +
+
+ +Make a `curl` `POST` request from your terminal to your application with various +emails to test the result. + +```shell +curl -X POST -d 'email=test@arcjet.io' http://localhost:3000/signup +``` diff --git a/src/snippets/signup-protection/quick-start/shared/Step2SetEnv.mdx b/src/snippets/signup-protection/quick-start/shared/Step2SetEnv.mdx index 5719eaa1..28e2915d 100644 --- a/src/snippets/signup-protection/quick-start/shared/Step2SetEnv.mdx +++ b/src/snippets/signup-protection/quick-start/shared/Step2SetEnv.mdx @@ -1,6 +1,8 @@ -Since Bun doesn't set `NODE_ENV` for you, you also need to set `ARCJET_ENV` in -your environment file. This allows Arcjet to accept a local IP address for -development purposes. +import FrameworkName from "@/components/FrameworkName"; + +Since doesn't set `NODE_ENV` for you, you also +need to set `ARCJET_ENV` in your environment file. This allows Arcjet to accept +a local IP address for development purposes. ```ini title=".env.local" # NODE_ENV is not set automatically, so tell Arcjet we're in dev diff --git a/src/snippets/signup-protection/reference/nestjs/CustomVerification.mdx b/src/snippets/signup-protection/reference/nestjs/CustomVerification.mdx new file mode 100644 index 00000000..752ec875 --- /dev/null +++ b/src/snippets/signup-protection/reference/nestjs/CustomVerification.mdx @@ -0,0 +1,9 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import CustomVerificationTS from "./CustomVerification.ts?raw"; + + +
+ +
+
diff --git a/src/snippets/signup-protection/reference/nestjs/CustomVerification.ts b/src/snippets/signup-protection/reference/nestjs/CustomVerification.ts new file mode 100644 index 00000000..56334428 --- /dev/null +++ b/src/snippets/signup-protection/reference/nestjs/CustomVerification.ts @@ -0,0 +1,163 @@ +import { + ARCJET, + ArcjetDecision, + type ArcjetNest, + protectSignup, +} from "@arcjet/nest"; +import { + Body, + Controller, + HttpException, + HttpStatus, + Inject, + Injectable, + Logger, + Post, + Req, + UseInterceptors, +} from "@nestjs/common"; +import { NoFilesInterceptor } from "@nestjs/platform-express"; +import { IsNotEmpty } from "class-validator"; +import type { Request } from "express"; + +// Validation class as described at +// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail +// decorator here because Arcjet handles this for you. +export class SignupDto { + @IsNotEmpty() + // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned + email: string; +} + +// This would normally go in your service file e.g. +// src/signup/signup.service.ts +@Injectable() +export class SignupService { + private readonly logger = new Logger(SignupService.name); + + signup(email: string): { message: string } { + this.logger.log(`Form submission: ${email}`); + + return { + message: "Hello world", + }; + } +} + +// This would normally go in your controller file e.g. +// src/signup/signup.controller.ts + +// If the signup was coming from a proxy or Tor IP address this is suspicious, +// but we don't want to block them. Instead we will require manual verification +function isProxyOrTor(decision: ArcjetDecision): boolean { + for (const result of decision.results) { + if ( + result.reason.isBot() && + (decision.ip.isProxy() || decision.ip.isTor()) + ) { + return true; + } + } + return false; +} + +// If the signup email address was from a free provider we want to double check +// their details. +function isFreeEmail(decision: ArcjetDecision): boolean { + for (const result of decision.results) { + if (result.reason.isEmail() && result.reason.emailTypes.includes("FREE")) { + return true; + } + } + return false; +} + +@Controller("signup") +export class SignupController { + private readonly logger = new Logger(SignupController.name); + + constructor( + private readonly signupService: SignupService, + @Inject(ARCJET) private readonly arcjet: ArcjetNest, + ) {} + + // Implement a form handler following + // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't + // compatible with the NestJS Fastify adapter. + @Post() + @UseInterceptors(NoFilesInterceptor()) + async index(@Req() req: Request, @Body() body: SignupDto) { + const decision = await this.arcjet + .withRule( + protectSignup({ + email: { + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + // Block emails that are disposable, invalid, or have no MX records + block: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], + }, + bots: { + mode: "LIVE", + // configured with a list of bots to allow from + // https://arcjet.com/bot-list + allow: ["CURL"], // prevents bots from submitting the form, but allow curl for this example + }, + // It would be unusual for a form to be submitted more than 5 times in 10 + // minutes from the same IP address + rateLimit: { + // uses a sliding window rate limit + mode: "LIVE", + interval: "2m", // counts requests over a 10 minute sliding window + max: 5, // allows 5 submissions within the window + }, + }), + ) + .protect(req, { email: body.email }); + + this.logger.log(`Arcjet: id = ${decision.id}`); + this.logger.log(`Arcjet: decision = ${decision.conclusion}`); + + if (decision.isDenied()) { + if (decision.reason.isBot()) { + throw new HttpException("No bots allowed", HttpStatus.FORBIDDEN); + } else if (decision.reason.isRateLimit()) { + throw new HttpException( + "Too many requests", + HttpStatus.TOO_MANY_REQUESTS, + ); + } else if (decision.reason.isEmail()) { + this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`); + + let message: string; + + // These are specific errors to help the user, but will also reveal the + // validation to a spammer. + if (decision.reason.emailTypes.includes("INVALID")) { + message = "email address format is invalid. Is there a typo?"; + } else if (decision.reason.emailTypes.includes("DISPOSABLE")) { + message = "we do not allow disposable email addresses."; + } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) { + message = + "your email domain does not have an MX record. Is there a typo?"; + } else { + // This is a catch all, but the above should be exhaustive based on the + // configured rules. + message = "invalid email."; + } + + throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST); + } else { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); + } + } + + // At this point the signup is allowed, but we may want to take additional + // verification steps + + const requireAdditionalVerification = + isProxyOrTor(decision) || isFreeEmail(decision); + + // User creation code goes here... + + return this.signupService.signup(body.email); + } +} diff --git a/src/snippets/signup-protection/reference/nestjs/Errors.mdx b/src/snippets/signup-protection/reference/nestjs/Errors.mdx new file mode 100644 index 00000000..1ee27666 --- /dev/null +++ b/src/snippets/signup-protection/reference/nestjs/Errors.mdx @@ -0,0 +1,9 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import ErrorsTS from "./Errors.ts?raw"; + + +
+ +
+
diff --git a/src/snippets/signup-protection/reference/nestjs/Errors.ts b/src/snippets/signup-protection/reference/nestjs/Errors.ts new file mode 100644 index 00000000..45403f60 --- /dev/null +++ b/src/snippets/signup-protection/reference/nestjs/Errors.ts @@ -0,0 +1,137 @@ +import { ARCJET, type ArcjetNest, protectSignup } from "@arcjet/nest"; +import { + Body, + Controller, + HttpException, + HttpStatus, + Inject, + Injectable, + Logger, + Post, + Req, + UseInterceptors, +} from "@nestjs/common"; +import { NoFilesInterceptor } from "@nestjs/platform-express"; +import { IsNotEmpty } from "class-validator"; +import type { Request } from "express"; + +// Validation class as described at +// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail +// decorator here because Arcjet handles this for you. +export class SignupDto { + @IsNotEmpty() + // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned + email: string; +} + +// This would normally go in your service file e.g. +// src/signup/signup.service.ts +@Injectable() +export class SignupService { + private readonly logger = new Logger(SignupService.name); + + signup(email: string): { message: string } { + this.logger.log(`Form submission: ${email}`); + + return { + message: "Hello world", + }; + } +} + +// This would normally go in your controller file e.g. +// src/signup/signup.controller.ts +@Controller("signup") +export class SignupController { + private readonly logger = new Logger(SignupController.name); + + constructor( + private readonly signupService: SignupService, + @Inject(ARCJET) private readonly arcjet: ArcjetNest, + ) {} + + // Implement a form handler following + // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't + // compatible with the NestJS Fastify adapter. + @Post() + @UseInterceptors(NoFilesInterceptor()) + async index(@Req() req: Request, @Body() body: SignupDto) { + const decision = await this.arcjet + .withRule( + protectSignup({ + email: { + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + // Block emails that are disposable, invalid, or have no MX records + block: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], + }, + bots: { + mode: "LIVE", + // configured with a list of bots to allow from + // https://arcjet.com/bot-list + allow: ["CURL"], // prevents bots from submitting the form, but allow curl for this example + }, + // It would be unusual for a form to be submitted more than 5 times in 10 + // minutes from the same IP address + rateLimit: { + // uses a sliding window rate limit + mode: "LIVE", + interval: "2m", // counts requests over a 10 minute sliding window + max: 5, // allows 5 submissions within the window + }, + }), + ) + .protect(req, { email: body.email }); + + this.logger.log(`Arcjet: id = ${decision.id}`); + this.logger.log(`Arcjet: decision = ${decision.conclusion}`); + + if (decision.isDenied()) { + if (decision.reason.isBot()) { + throw new HttpException("No bots allowed", HttpStatus.FORBIDDEN); + } else if (decision.reason.isRateLimit()) { + throw new HttpException( + "Too many requests", + HttpStatus.TOO_MANY_REQUESTS, + ); + } else if (decision.reason.isEmail()) { + this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`); + + let message: string; + + // These are specific errors to help the user, but will also reveal the + // validation to a spammer. + if (decision.reason.emailTypes.includes("INVALID")) { + message = "email address format is invalid. Is there a typo?"; + } else if (decision.reason.emailTypes.includes("DISPOSABLE")) { + message = "we do not allow disposable email addresses."; + } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) { + message = + "your email domain does not have an MX record. Is there a typo?"; + } else { + // This is a catch all, but the above should be exhaustive based on the + // configured rules. + message = "invalid email."; + } + + throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST); + } else { + throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); + } + } else if (decision.isErrored()) { + if (decision.reason.message.includes("missing User-Agent header")) { + // Requests without User-Agent headers can not be identified as any + // particular bot and will be marked as an errored decision. Most + // legitimate clients always send this header, so we recommend blocking + // requests without it. + this.logger.warn("User-Agent header is missing"); + throw new HttpException("Bad request", HttpStatus.BAD_REQUEST); + } else { + // 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: ${decision.reason.message}`); + } + } + + return this.signupService.signup(body.email); + } +} diff --git a/src/snippets/signup-protection/reference/nestjs/Recommended.mdx b/src/snippets/signup-protection/reference/nestjs/Recommended.mdx new file mode 100644 index 00000000..da9133ce --- /dev/null +++ b/src/snippets/signup-protection/reference/nestjs/Recommended.mdx @@ -0,0 +1,9 @@ +import SelectableContent from "@/components/SelectableContent"; +import { Code } from "@astrojs/starlight/components"; +import RecommendedTS from "./Recommended.ts?raw"; + + +
+ +
+
diff --git a/src/snippets/signup-protection/reference/nestjs/Recommended.ts b/src/snippets/signup-protection/reference/nestjs/Recommended.ts new file mode 100644 index 00000000..34cd8179 --- /dev/null +++ b/src/snippets/signup-protection/reference/nestjs/Recommended.ts @@ -0,0 +1,23 @@ +import { protectSignup } from "@arcjet/nest"; + +protectSignup({ + email: { + mode: "LIVE", // will block requests. Use "DRY_RUN" to log only + // Block emails that are disposable, invalid, or have no MX records + block: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], + }, + bots: { + mode: "LIVE", + // configured with a list of bots to allow from + // https://arcjet.com/bot-list + allow: [], // "allow none" will block all detected bots + }, + // It would be unusual for a form to be submitted more than 5 times in 10 + // minutes from the same IP address + rateLimit: { + // uses a sliding window rate limit + mode: "LIVE", + interval: "10m", // counts requests over a 10 minute sliding window + max: 5, // allows 5 submissions within the window + }, +});