@arcjet/decorate 1.7.0-rc.0 → 1.7.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.
package/index.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { ArcjetDecision } from "@arcjet/protocol";
2
+ interface HeaderLike {
3
+ get(name: string): string | null;
4
+ has(name: string): boolean;
5
+ set(name: string, value: string): void;
6
+ }
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;
15
+ }
16
+ /**
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
29
+ * HTTP](https://ietf-wg-httpapi.github.io/ratelimit-headers/draft-ietf-httpapi-ratelimit-headers.html)
30
+ * draft specification.
31
+ *
32
+ * @param value
33
+ * Decorable value.
34
+ * @param decision
35
+ * Decision from `protect()`.
36
+ * @returns
37
+ * Nothing.
38
+ */
39
+ export declare function setRateLimitHeaders(value: ArcjetCanDecorate, decision: ArcjetDecision): void;
40
+ export {};
package/index.js ADDED
@@ -0,0 +1,179 @@
1
+ import { sprintf } from '@arcjet/sprintf';
2
+ import '@arcjet/protocol';
3
+
4
+ function isHeaderLike(value) {
5
+ if ("has" in value &&
6
+ typeof value.has === "function" &&
7
+ "get" in value &&
8
+ typeof value.get === "function" &&
9
+ "set" in value &&
10
+ typeof value.set === "function") {
11
+ return true;
12
+ }
13
+ return false;
14
+ }
15
+ function isResponseLike(value) {
16
+ if (!("headers" in value) || typeof value.headers === "undefined") {
17
+ return false;
18
+ }
19
+ return isHeaderLike(value.headers);
20
+ }
21
+ function isOutgoingMessageLike(response) {
22
+ if (!("headersSent" in response) ||
23
+ typeof response.headersSent !== "boolean") {
24
+ return false;
25
+ }
26
+ if (typeof response.hasHeader !== "function") {
27
+ return false;
28
+ }
29
+ if (typeof response.getHeader !== "function") {
30
+ return false;
31
+ }
32
+ if (typeof response.setHeader !== "function") {
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ function sortByLowestMax([maxA, _windowA], [maxB, _windowB]) {
38
+ return maxA - maxB;
39
+ }
40
+ function toPolicyString([max, window]) {
41
+ return `${max};w=${window}`;
42
+ }
43
+ function toLimitString({ max, remaining, reset, }) {
44
+ return `limit=${max}, remaining=${remaining}, reset=${reset}`;
45
+ }
46
+ function extractReason(result) {
47
+ return result.reason;
48
+ }
49
+ function isRateLimitReason(reason) {
50
+ return reason.isRateLimit();
51
+ }
52
+ function nearestLimit(current, next) {
53
+ if (current.remaining < next.remaining) {
54
+ return current;
55
+ }
56
+ if (current.remaining > next.remaining) {
57
+ return next;
58
+ }
59
+ // Reaching here means `remaining` is equal so prioritize closest reset
60
+ if (current.reset < next.reset) {
61
+ return current;
62
+ }
63
+ if (current.reset > next.reset) {
64
+ return next;
65
+ }
66
+ // Reaching here means that `remaining` and `reset` are equal, so prioritize
67
+ // the smallest `max`
68
+ if (current.max < next.max) {
69
+ return current;
70
+ }
71
+ // All else equal, just return the next item in the list
72
+ return next;
73
+ }
74
+ /**
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
79
+ * HTTP](https://ietf-wg-httpapi.github.io/ratelimit-headers/draft-ietf-httpapi-ratelimit-headers.html)
80
+ * draft specification.
81
+ *
82
+ * @param value
83
+ * Decorable value.
84
+ * @param decision
85
+ * Decision from `protect()`.
86
+ * @returns
87
+ * Nothing.
88
+ */
89
+ function setRateLimitHeaders(value, decision) {
90
+ const rateLimitReasons = decision.results
91
+ .map(extractReason)
92
+ .filter(isRateLimitReason);
93
+ let policy;
94
+ let limit;
95
+ if (rateLimitReasons.length > 0) {
96
+ const policies = new Map();
97
+ for (const reason of rateLimitReasons) {
98
+ if (policies.has(reason.max)) {
99
+ console.error("Invalid rate limit policy—two policies should not share the same limit");
100
+ return;
101
+ }
102
+ if (typeof reason.max !== "number" ||
103
+ typeof reason.window !== "number" ||
104
+ typeof reason.remaining !== "number" ||
105
+ typeof reason.reset !== "number") {
106
+ console.error(sprintf("Invalid rate limit encountered: %o", reason));
107
+ return;
108
+ }
109
+ policies.set(reason.max, reason.window);
110
+ }
111
+ const rl = rateLimitReasons.reduce(nearestLimit);
112
+ limit = toLimitString(rl);
113
+ policy = Array.from(policies.entries())
114
+ .sort(sortByLowestMax)
115
+ .map(toPolicyString)
116
+ .join(", ");
117
+ }
118
+ else {
119
+ // For cached decisions, we may not have rule results, but we'd still have
120
+ // the top-level reason.
121
+ if (isRateLimitReason(decision.reason)) {
122
+ if (typeof decision.reason.max !== "number" ||
123
+ typeof decision.reason.window !== "number" ||
124
+ typeof decision.reason.remaining !== "number" ||
125
+ typeof decision.reason.reset !== "number") {
126
+ console.error(sprintf("Invalid rate limit encountered: %o", decision.reason));
127
+ return;
128
+ }
129
+ limit = toLimitString(decision.reason);
130
+ policy = toPolicyString([decision.reason.max, decision.reason.window]);
131
+ }
132
+ else {
133
+ return;
134
+ }
135
+ }
136
+ if (isHeaderLike(value)) {
137
+ if (value.has("RateLimit")) {
138
+ console.warn(sprintf("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.get("RateLimit"), limit));
139
+ }
140
+ if (value.has("RateLimit-Policy")) {
141
+ console.warn(sprintf("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.get("RateLimit-Policy"), limit));
142
+ }
143
+ value.set("RateLimit", limit);
144
+ value.set("RateLimit-Policy", policy);
145
+ // The response was handled
146
+ return;
147
+ }
148
+ if (isResponseLike(value)) {
149
+ if (value.headers.has("RateLimit")) {
150
+ console.warn(sprintf("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.headers.get("RateLimit"), limit));
151
+ }
152
+ if (value.headers.has("RateLimit-Policy")) {
153
+ console.warn(sprintf("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.headers.get("RateLimit-Policy"), limit));
154
+ }
155
+ value.headers.set("RateLimit", limit);
156
+ value.headers.set("RateLimit-Policy", policy);
157
+ // The response was handled
158
+ return;
159
+ }
160
+ if (isOutgoingMessageLike(value)) {
161
+ if (value.headersSent) {
162
+ console.error("Headers have already been sent—cannot set RateLimit header");
163
+ return;
164
+ }
165
+ if (value.hasHeader("RateLimit")) {
166
+ console.warn(sprintf("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.getHeader("RateLimit"), limit));
167
+ }
168
+ if (value.hasHeader("RateLimit-Policy")) {
169
+ console.warn(sprintf("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.getHeader("RateLimit-Policy"), limit));
170
+ }
171
+ value.setHeader("RateLimit", limit);
172
+ value.setHeader("RateLimit-Policy", policy);
173
+ // The response was handled
174
+ return;
175
+ }
176
+ console.debug("Cannot determine if response is Response or OutgoingMessage type");
177
+ }
178
+
179
+ export { setRateLimitHeaders };
package/package.json CHANGED
@@ -1,63 +1,60 @@
1
1
  {
2
2
  "name": "@arcjet/decorate",
3
- "version": "1.7.0-rc.0",
3
+ "version": "1.7.0",
4
4
  "description": "Arcjet utilities for decorating responses with information",
5
5
  "keywords": [
6
6
  "arcjet",
7
7
  "decore",
8
- "util",
9
- "utility"
8
+ "utility",
9
+ "util"
10
10
  ],
11
+ "license": "Apache-2.0",
11
12
  "homepage": "https://arcjet.com",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/arcjet/arcjet-js.git",
16
+ "directory": "decorate"
17
+ },
12
18
  "bugs": {
13
19
  "url": "https://github.com/arcjet/arcjet-js/issues",
14
20
  "email": "support@arcjet.com"
15
21
  },
16
- "license": "Apache-2.0",
17
22
  "author": {
18
23
  "name": "Arcjet",
19
24
  "email": "support@arcjet.com",
20
25
  "url": "https://arcjet.com"
21
26
  },
22
- "repository": {
23
- "type": "git",
24
- "url": "git+https://github.com/arcjet/arcjet-js.git",
25
- "directory": "decorate"
27
+ "engines": {
28
+ "node": ">=22.21.0 <23 || >=24.5.0"
26
29
  },
30
+ "type": "module",
31
+ "main": "./index.js",
32
+ "types": "./index.d.ts",
27
33
  "files": [
28
- "dist"
34
+ "index.d.ts",
35
+ "index.js"
29
36
  ],
30
- "type": "module",
31
- "main": "./dist/index.js",
32
- "types": "./dist/index.d.ts",
33
- "exports": {
34
- ".": {
35
- "types": "./dist/index.d.ts",
36
- "default": "./dist/index.js"
37
- },
38
- "./package.json": "./package.json"
39
- },
40
- "publishConfig": {
41
- "access": "public",
42
- "tag": "latest"
43
- },
44
37
  "scripts": {
45
- "build": "tsdown",
46
- "typecheck": "tsgo --noEmit",
47
- "test-api": "node --test -- test/*.test.ts",
48
- "test-coverage": "node --experimental-test-coverage --test -- test/*.test.ts",
49
- "test": "npm run build && npm run test-coverage"
38
+ "build": "rollup --config rollup.config.js",
39
+ "lint": "eslint .",
40
+ "test-api": "node --test -- test/*.test.js",
41
+ "test-coverage": "node --experimental-test-coverage --test -- test/*.test.js",
42
+ "test": "npm run build && npm run lint && npm run test-coverage"
50
43
  },
51
44
  "dependencies": {
52
- "@arcjet/protocol": "1.7.0-rc.0",
53
- "@arcjet/sprintf": "1.7.0-rc.0"
45
+ "@arcjet/protocol": "1.7.0",
46
+ "@arcjet/sprintf": "1.7.0"
54
47
  },
55
48
  "devDependencies": {
49
+ "@arcjet/eslint-config": "1.7.0",
50
+ "@arcjet/rollup-config": "1.7.0",
51
+ "@rollup/wasm-node": "4.62.2",
56
52
  "@types/node": "22.19.21",
57
- "tsdown": "0.22.3",
58
- "typescript": "6.0.3"
53
+ "eslint": "9.39.4",
54
+ "typescript": "5.9.3"
59
55
  },
60
- "engines": {
61
- "node": ">=22.21.0 <23 || >=24.5.0"
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "tag": "latest"
62
59
  }
63
60
  }
package/dist/index.d.ts DELETED
@@ -1,43 +0,0 @@
1
- import { ArcjetDecision } from "@arcjet/protocol";
2
-
3
- //#region src/index.d.ts
4
- interface HeaderLike {
5
- get(name: string): string | null;
6
- has(name: string): boolean;
7
- set(name: string, value: string): void;
8
- }
9
- interface ResponseLike {
10
- headers: HeaderLike;
11
- }
12
- interface OutgoingMessageLike {
13
- getHeader: (name: string) => string[] | number | string | undefined;
14
- headersSent: boolean;
15
- hasHeader: (name: string) => boolean;
16
- setHeader: (name: string, value: ReadonlyArray<string> | number | string) => unknown;
17
- }
18
- /**
19
- * Decorable value.
20
- *
21
- * Something that looks like `Headers` (Fetch),
22
- * `OutgoingMessage` (Node.js), or
23
- * `Response` (Fetch).
24
- */
25
- type ArcjetCanDecorate = HeaderLike | OutgoingMessageLike | ResponseLike;
26
- /**
27
- * Decorate something based on an Arcjet decision with rate limit headers.
28
- *
29
- * Sets `RateLimit-Policy` and `RateLimit` and conform to the
30
- * [Rate Limit fields for
31
- * HTTP](https://ietf-wg-httpapi.github.io/ratelimit-headers/draft-ietf-httpapi-ratelimit-headers.html)
32
- * draft specification.
33
- *
34
- * @param value
35
- * Decorable value.
36
- * @param decision
37
- * Decision from `protect()`.
38
- * @returns
39
- * Nothing.
40
- */
41
- declare function setRateLimitHeaders(value: ArcjetCanDecorate, decision: ArcjetDecision): void;
42
- //#endregion
43
- export { ArcjetCanDecorate, setRateLimitHeaders };
package/dist/index.js DELETED
@@ -1,112 +0,0 @@
1
- import "@arcjet/protocol";
2
- import { sprintf } from "@arcjet/sprintf";
3
- //#region src/index.ts
4
- function isHeaderLike(value) {
5
- if ("has" in value && typeof value.has === "function" && "get" in value && typeof value.get === "function" && "set" in value && typeof value.set === "function") return true;
6
- return false;
7
- }
8
- function isResponseLike(value) {
9
- if (!("headers" in value) || typeof value.headers === "undefined") return false;
10
- return isHeaderLike(value.headers);
11
- }
12
- function isOutgoingMessageLike(response) {
13
- if (!("headersSent" in response) || typeof response.headersSent !== "boolean") return false;
14
- if (typeof response.hasHeader !== "function") return false;
15
- if (typeof response.getHeader !== "function") return false;
16
- if (typeof response.setHeader !== "function") return false;
17
- return true;
18
- }
19
- function sortByLowestMax([maxA, _windowA], [maxB, _windowB]) {
20
- return maxA - maxB;
21
- }
22
- function toPolicyString([max, window]) {
23
- return `${max};w=${window}`;
24
- }
25
- function toLimitString({ max, remaining, reset }) {
26
- return `limit=${max}, remaining=${remaining}, reset=${reset}`;
27
- }
28
- function extractReason(result) {
29
- return result.reason;
30
- }
31
- function isRateLimitReason(reason) {
32
- return reason.isRateLimit();
33
- }
34
- function nearestLimit(current, next) {
35
- if (current.remaining < next.remaining) return current;
36
- if (current.remaining > next.remaining) return next;
37
- if (current.reset < next.reset) return current;
38
- if (current.reset > next.reset) return next;
39
- if (current.max < next.max) return current;
40
- return next;
41
- }
42
- /**
43
- * Decorate something based on an Arcjet decision with rate limit headers.
44
- *
45
- * Sets `RateLimit-Policy` and `RateLimit` and conform to the
46
- * [Rate Limit fields for
47
- * HTTP](https://ietf-wg-httpapi.github.io/ratelimit-headers/draft-ietf-httpapi-ratelimit-headers.html)
48
- * draft specification.
49
- *
50
- * @param value
51
- * Decorable value.
52
- * @param decision
53
- * Decision from `protect()`.
54
- * @returns
55
- * Nothing.
56
- */
57
- function setRateLimitHeaders(value, decision) {
58
- const rateLimitReasons = decision.results.map(extractReason).filter(isRateLimitReason);
59
- let policy;
60
- let limit;
61
- if (rateLimitReasons.length > 0) {
62
- const policies = /* @__PURE__ */ new Map();
63
- for (const reason of rateLimitReasons) {
64
- if (policies.has(reason.max)) {
65
- console.error("Invalid rate limit policy—two policies should not share the same limit");
66
- return;
67
- }
68
- if (typeof reason.max !== "number" || typeof reason.window !== "number" || typeof reason.remaining !== "number" || typeof reason.reset !== "number") {
69
- console.error(sprintf("Invalid rate limit encountered: %o", reason));
70
- return;
71
- }
72
- policies.set(reason.max, reason.window);
73
- }
74
- limit = toLimitString(rateLimitReasons.reduce(nearestLimit));
75
- policy = Array.from(policies.entries()).sort(sortByLowestMax).map(toPolicyString).join(", ");
76
- } else if (isRateLimitReason(decision.reason)) {
77
- if (typeof decision.reason.max !== "number" || typeof decision.reason.window !== "number" || typeof decision.reason.remaining !== "number" || typeof decision.reason.reset !== "number") {
78
- console.error(sprintf("Invalid rate limit encountered: %o", decision.reason));
79
- return;
80
- }
81
- limit = toLimitString(decision.reason);
82
- policy = toPolicyString([decision.reason.max, decision.reason.window]);
83
- } else return;
84
- if (isHeaderLike(value)) {
85
- if (value.has("RateLimit")) console.warn(sprintf("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.get("RateLimit"), limit));
86
- if (value.has("RateLimit-Policy")) console.warn(sprintf("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.get("RateLimit-Policy"), limit));
87
- value.set("RateLimit", limit);
88
- value.set("RateLimit-Policy", policy);
89
- return;
90
- }
91
- if (isResponseLike(value)) {
92
- if (value.headers.has("RateLimit")) console.warn(sprintf("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.headers.get("RateLimit"), limit));
93
- if (value.headers.has("RateLimit-Policy")) console.warn(sprintf("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.headers.get("RateLimit-Policy"), limit));
94
- value.headers.set("RateLimit", limit);
95
- value.headers.set("RateLimit-Policy", policy);
96
- return;
97
- }
98
- if (isOutgoingMessageLike(value)) {
99
- if (value.headersSent) {
100
- console.error("Headers have already been sent—cannot set RateLimit header");
101
- return;
102
- }
103
- if (value.hasHeader("RateLimit")) console.warn(sprintf("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.getHeader("RateLimit"), limit));
104
- if (value.hasHeader("RateLimit-Policy")) console.warn(sprintf("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.getHeader("RateLimit-Policy"), limit));
105
- value.setHeader("RateLimit", limit);
106
- value.setHeader("RateLimit-Policy", policy);
107
- return;
108
- }
109
- console.debug("Cannot determine if response is Response or OutgoingMessage type");
110
- }
111
- //#endregion
112
- export { setRateLimitHeaders };