@arcjet/decorate 1.0.0-beta.9 → 1.1.0-rc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +47 -24
  2. package/index.d.ts +27 -15
  3. package/index.js +13 -7
  4. package/package.json +12 -14
package/README.md CHANGED
@@ -21,53 +21,76 @@
21
21
  - [npm package (`@arcjet/decorate`)](https://www.npmjs.com/package/@arcjet/decorate)
22
22
  - [GitHub source code (`decorate/` in `arcjet/arcjet-js`)](https://github.com/arcjet/arcjet-js/tree/main/decorate)
23
23
 
24
- ## Installation
24
+ ## What is this?
25
25
 
26
- ```shell
27
- npm install -S @arcjet/decorate
26
+ This is a utility that lets you decorate responses based on Arcjet decisions.
27
+ It currently supports experimental rate limit headers.
28
+
29
+ ## When should I use this?
30
+
31
+ You can use this package if you use the rate limit rule and want to set
32
+ experimental `RateLimit-Policy` and `RateLimit` headers.
33
+ See [_RateLimit header fields for HTTP_ on `ietf.org`][ietf-rate-limit] for
34
+ more info.
35
+
36
+ ## Install
37
+
38
+ This package is ESM only.
39
+ Install with npm in Node.js:
40
+
41
+ ```sh
42
+ npm install @arcjet/decorate
28
43
  ```
29
44
 
30
45
  ## Example
31
46
 
32
47
  ```ts
33
- import arcjet, { fixedWindow } from "@arcjet/next";
48
+ import http from "node:http";
34
49
  import { setRateLimitHeaders } from "@arcjet/decorate";
35
- import { NextApiRequest, NextApiResponse } from "next";
50
+ import arcjet, { shield } from "@arcjet/node";
51
+
52
+ // Get your Arcjet key at <https://app.arcjet.com>.
53
+ // Set it as an environment variable instead of hard coding it.
54
+ const arcjetKey = process.env.ARCJET_KEY;
55
+
56
+ if (!arcjetKey) {
57
+ throw new Error("Cannot find `ARCJET_KEY` environment variable");
58
+ }
36
59
 
37
60
  const aj = arcjet({
38
- key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
61
+ key: arcjetKey,
39
62
  rules: [
40
- // Create a fixed window rate limit. Other algorithms are supported.
41
- fixedWindow({
42
- mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
43
- window: "1m", // 1 min fixed window
44
- max: 1, // allow a single request
45
- }),
63
+ // Shield protects your app from common attacks.
64
+ // Use `DRY_RUN` instead of `LIVE` to only log.
65
+ shield({ mode: "LIVE" }),
46
66
  ],
47
67
  });
48
68
 
49
- export default async function handler(
50
- req: NextApiRequest,
51
- res: NextApiResponse,
69
+ const server = http.createServer(async function (
70
+ request: http.IncomingMessage,
71
+ response: http.ServerResponse,
52
72
  ) {
53
- const decision = await aj.protect(req);
73
+ const decision = await aj.protect(request);
54
74
 
55
- setRateLimitHeaders(res, decision);
75
+ setRateLimitHeaders(response, decision);
56
76
 
57
77
  if (decision.isDenied()) {
58
- return res.status(429).json({
59
- error: "Too Many Requests",
60
- reason: decision.reason,
61
- });
78
+ response.writeHead(403, { "Content-Type": "application/json" });
79
+ response.end(JSON.stringify({ message: "Forbidden" }));
80
+ return;
62
81
  }
63
82
 
64
- res.status(200).json({ name: "Hello world" });
65
- }
83
+ response.writeHead(200, { "Content-Type": "application/json" });
84
+ response.end(JSON.stringify({ message: "Hello world" }));
85
+ });
86
+
87
+ server.listen(8000);
66
88
  ```
67
89
 
68
90
  ## License
69
91
 
70
- Licensed under the [Apache License, Version 2.0][apache-license].
92
+ [Apache License, Version 2.0][apache-license] © [Arcjet Labs, Inc.][arcjet]
71
93
 
72
94
  [arcjet]: https://arcjet.com
73
95
  [apache-license]: http://www.apache.org/licenses/LICENSE-2.0
96
+ [ietf-rate-limit]: https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-08
package/index.d.ts CHANGED
@@ -1,28 +1,40 @@
1
1
  import type { ArcjetDecision } from "@arcjet/protocol";
2
2
  interface HeaderLike {
3
- has(name: string): boolean;
4
3
  get(name: string): string | null;
4
+ has(name: string): boolean;
5
5
  set(name: string, value: string): void;
6
6
  }
7
- export interface ArcjetCanDecorate {
8
- has?: (name: string) => boolean;
9
- get?: (name: string) => string | null;
10
- set?: (name: string, value: string) => void;
11
- headers?: HeaderLike;
12
- headersSent?: boolean;
13
- hasHeader?: (name: string) => boolean;
14
- getHeader?: (name: string) => number | string | string[] | undefined;
15
- setHeader?: (name: string, value: number | string | ReadonlyArray<string>) => unknown;
7
+ interface ResponseLike {
8
+ headers: HeaderLike;
9
+ }
10
+ interface OutgoingMessageLike {
11
+ getHeader: (name: string) => string[] | number | string | undefined;
12
+ headersSent: boolean;
13
+ hasHeader: (name: string) => boolean;
14
+ setHeader: (name: string, value: ReadonlyArray<string> | number | string) => unknown;
16
15
  }
17
16
  /**
18
- * Decorates an object with `RateLimit` and `RateLimit-Policy` headers based
19
- * on an {@link ArcjetDecision} and conforming to the [Rate Limit fields for
17
+ * Decorable value.
18
+ *
19
+ * Something that looks like `Headers` (Fetch),
20
+ * `OutgoingMessage` (Node.js), or
21
+ * `Response` (Fetch).
22
+ */
23
+ export type ArcjetCanDecorate = HeaderLike | OutgoingMessageLike | ResponseLike;
24
+ /**
25
+ * Decorate something based on an Arcjet decision with rate limit headers.
26
+ *
27
+ * Sets `RateLimit-Policy` and `RateLimit` and conform to the
28
+ * [Rate Limit fields for
20
29
  * HTTP](https://ietf-wg-httpapi.github.io/ratelimit-headers/draft-ietf-httpapi-ratelimit-headers.html)
21
30
  * draft specification.
22
31
  *
23
- * @param value The object to decorate—must be similar to {@link Headers}, {@link Response} or
24
- * {@link OutgoingMessage}.
25
- * @param decision The {@link ArcjetDecision} that was made by calling `protect()` on the SDK.
32
+ * @param value
33
+ * Decorable value.
34
+ * @param decision
35
+ * Decision from `protect()`.
36
+ * @returns
37
+ * Nothing.
26
38
  */
27
39
  export declare function setRateLimitHeaders(value: ArcjetCanDecorate, decision: ArcjetDecision): void;
28
40
  export {};
package/index.js CHANGED
@@ -13,13 +13,14 @@ function isHeaderLike(value) {
13
13
  return false;
14
14
  }
15
15
  function isResponseLike(value) {
16
- if (typeof value.headers === "undefined") {
16
+ if (!("headers" in value) || typeof value.headers === "undefined") {
17
17
  return false;
18
18
  }
19
19
  return isHeaderLike(value.headers);
20
20
  }
21
21
  function isOutgoingMessageLike(response) {
22
- if (typeof response.headersSent !== "boolean") {
22
+ if (!("headersSent" in response) ||
23
+ typeof response.headersSent !== "boolean") {
23
24
  return false;
24
25
  }
25
26
  if (typeof response.hasHeader !== "function") {
@@ -71,14 +72,19 @@ function nearestLimit(current, next) {
71
72
  return next;
72
73
  }
73
74
  /**
74
- * Decorates an object with `RateLimit` and `RateLimit-Policy` headers based
75
- * on an {@link ArcjetDecision} and conforming to the [Rate Limit fields for
75
+ * Decorate something based on an Arcjet decision with rate limit headers.
76
+ *
77
+ * Sets `RateLimit-Policy` and `RateLimit` and conform to the
78
+ * [Rate Limit fields for
76
79
  * HTTP](https://ietf-wg-httpapi.github.io/ratelimit-headers/draft-ietf-httpapi-ratelimit-headers.html)
77
80
  * draft specification.
78
81
  *
79
- * @param value The object to decorate—must be similar to {@link Headers}, {@link Response} or
80
- * {@link OutgoingMessage}.
81
- * @param decision The {@link ArcjetDecision} that was made by calling `protect()` on the SDK.
82
+ * @param value
83
+ * Decorable value.
84
+ * @param decision
85
+ * Decision from `protect()`.
86
+ * @returns
87
+ * Nothing.
82
88
  */
83
89
  function setRateLimitHeaders(value, decision) {
84
90
  const rateLimitReasons = decision.results
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcjet/decorate",
3
- "version": "1.0.0-beta.9",
3
+ "version": "1.1.0-rc",
4
4
  "description": "Arcjet utilities for decorating responses with information",
5
5
  "keywords": [
6
6
  "arcjet",
@@ -25,7 +25,7 @@
25
25
  "url": "https://arcjet.com"
26
26
  },
27
27
  "engines": {
28
- "node": ">=18"
28
+ "node": ">=20"
29
29
  },
30
30
  "type": "module",
31
31
  "main": "./index.js",
@@ -37,23 +37,21 @@
37
37
  "scripts": {
38
38
  "build": "rollup --config rollup.config.js",
39
39
  "lint": "eslint .",
40
- "prepublishOnly": "npm run build",
41
- "test-api": "node --test",
42
- "test-coverage": "node --experimental-test-coverage --test",
40
+ "test-api": "node --test -- test/*.test.js",
41
+ "test-coverage": "node --experimental-test-coverage --test -- test/*.test.js",
43
42
  "test": "npm run build && npm run lint && npm run test-coverage"
44
43
  },
45
44
  "dependencies": {
46
- "@arcjet/protocol": "1.0.0-beta.9",
47
- "@arcjet/sprintf": "1.0.0-beta.9"
45
+ "@arcjet/protocol": "1.1.0-rc",
46
+ "@arcjet/sprintf": "1.1.0-rc"
48
47
  },
49
48
  "devDependencies": {
50
- "@arcjet/eslint-config": "1.0.0-beta.9",
51
- "@arcjet/rollup-config": "1.0.0-beta.9",
52
- "@arcjet/tsconfig": "1.0.0-beta.9",
53
- "@rollup/wasm-node": "4.44.2",
54
- "@types/node": "18.18.0",
55
- "eslint": "9.30.1",
56
- "typescript": "5.8.3"
49
+ "@arcjet/eslint-config": "1.1.0-rc",
50
+ "@arcjet/rollup-config": "1.1.0-rc",
51
+ "@rollup/wasm-node": "4.57.0",
52
+ "@types/node": "25.0.10",
53
+ "eslint": "9.39.2",
54
+ "typescript": "5.9.3"
57
55
  },
58
56
  "publishConfig": {
59
57
  "access": "public",