@arcjet/decorate 1.0.0-beta.8 → 1.0.0

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 +50 -24
  2. package/index.d.ts +27 -15
  3. package/index.js +13 -7
  4. package/package.json +22 -20
package/README.md CHANGED
@@ -18,53 +18,79 @@
18
18
 
19
19
  [Arcjet][arcjet] utilities for decorating responses with information.
20
20
 
21
- ## Installation
21
+ - [npm package (`@arcjet/decorate`)](https://www.npmjs.com/package/@arcjet/decorate)
22
+ - [GitHub source code (`decorate/` in `arcjet/arcjet-js`)](https://github.com/arcjet/arcjet-js/tree/main/decorate)
22
23
 
23
- ```shell
24
- npm install -S @arcjet/decorate
24
+ ## What is this?
25
+
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
25
43
  ```
26
44
 
27
45
  ## Example
28
46
 
29
47
  ```ts
30
- import arcjet, { fixedWindow } from "@arcjet/next";
48
+ import http from "node:http";
31
49
  import { setRateLimitHeaders } from "@arcjet/decorate";
32
- 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
+ }
33
59
 
34
60
  const aj = arcjet({
35
- key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
61
+ key: arcjetKey,
36
62
  rules: [
37
- // Create a fixed window rate limit. Other algorithms are supported.
38
- fixedWindow({
39
- mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
40
- window: "1m", // 1 min fixed window
41
- max: 1, // allow a single request
42
- }),
63
+ // Shield protects your app from common attacks.
64
+ // Use `DRY_RUN` instead of `LIVE` to only log.
65
+ shield({ mode: "LIVE" }),
43
66
  ],
44
67
  });
45
68
 
46
- export default async function handler(
47
- req: NextApiRequest,
48
- res: NextApiResponse,
69
+ const server = http.createServer(async function (
70
+ request: http.IncomingMessage,
71
+ response: http.ServerResponse,
49
72
  ) {
50
- const decision = await aj.protect(req);
73
+ const decision = await aj.protect(request);
51
74
 
52
- setRateLimitHeaders(res, decision);
75
+ setRateLimitHeaders(response, decision);
53
76
 
54
77
  if (decision.isDenied()) {
55
- return res.status(429).json({
56
- error: "Too Many Requests",
57
- reason: decision.reason,
58
- });
78
+ response.writeHead(403, { "Content-Type": "application/json" });
79
+ response.end(JSON.stringify({ message: "Forbidden" }));
80
+ return;
59
81
  }
60
82
 
61
- res.status(200).json({ name: "Hello world" });
62
- }
83
+ response.writeHead(200, { "Content-Type": "application/json" });
84
+ response.end(JSON.stringify({ message: "Hello world" }));
85
+ });
86
+
87
+ server.listen(8000);
63
88
  ```
64
89
 
65
90
  ## License
66
91
 
67
- Licensed under the [Apache License, Version 2.0][apache-license].
92
+ [Apache License, Version 2.0][apache-license] © [Arcjet Labs, Inc.][arcjet]
68
93
 
69
94
  [arcjet]: https://arcjet.com
70
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,7 +1,13 @@
1
1
  {
2
2
  "name": "@arcjet/decorate",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.0",
4
4
  "description": "Arcjet utilities for decorating responses with information",
5
+ "keywords": [
6
+ "arcjet",
7
+ "decore",
8
+ "utility",
9
+ "util"
10
+ ],
5
11
  "license": "Apache-2.0",
6
12
  "homepage": "https://arcjet.com",
7
13
  "repository": {
@@ -19,38 +25,34 @@
19
25
  "url": "https://arcjet.com"
20
26
  },
21
27
  "engines": {
22
- "node": ">=18"
28
+ "node": ">=20"
23
29
  },
24
30
  "type": "module",
25
31
  "main": "./index.js",
26
32
  "types": "./index.d.ts",
27
33
  "files": [
28
- "LICENSE",
29
- "README.md",
30
- "*.js",
31
- "*.d.ts",
32
- "!*.config.js"
34
+ "index.d.ts",
35
+ "index.js"
33
36
  ],
34
37
  "scripts": {
35
- "prepublishOnly": "npm run build",
36
38
  "build": "rollup --config rollup.config.js",
37
39
  "lint": "eslint .",
38
- "pretest": "npm run build",
39
- "test": "node --test --experimental-test-coverage"
40
+ "prepublishOnly": "npm run build",
41
+ "test-api": "node --test -- test/*.test.js",
42
+ "test-coverage": "node --experimental-test-coverage --test -- test/*.test.js",
43
+ "test": "npm run build && npm run lint && npm run test-coverage"
40
44
  },
41
45
  "dependencies": {
42
- "@arcjet/protocol": "1.0.0-beta.8",
43
- "@arcjet/sprintf": "1.0.0-beta.8"
46
+ "@arcjet/protocol": "1.0.0",
47
+ "@arcjet/sprintf": "1.0.0"
44
48
  },
45
49
  "devDependencies": {
46
- "@arcjet/eslint-config": "1.0.0-beta.8",
47
- "@arcjet/rollup-config": "1.0.0-beta.8",
48
- "@arcjet/tsconfig": "1.0.0-beta.8",
49
- "@rollup/wasm-node": "4.41.1",
50
- "@types/node": "18.18.0",
51
- "eslint": "9.27.0",
52
- "expect": "29.7.0",
53
- "typescript": "5.8.3"
50
+ "@arcjet/eslint-config": "1.0.0",
51
+ "@arcjet/rollup-config": "1.0.0",
52
+ "@rollup/wasm-node": "4.55.1",
53
+ "@types/node": "25.0.8",
54
+ "eslint": "9.39.2",
55
+ "typescript": "5.9.3"
54
56
  },
55
57
  "publishConfig": {
56
58
  "access": "public",