@arcjet/decorate 1.0.0-alpha.9 → 1.0.0-beta.1

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <a href="https://arcjet.com" target="_arcjet-home">
2
2
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://arcjet.com/arcjet-logo-dark-planet-arrival.svg">
4
- <img src="https://arcjet.com/arcjet-logo-light-planet-arrival.svg" alt="Arcjet Logo" height="144" width="auto">
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://arcjet.com/logo/arcjet-dark-lockup-voyage-horizontal.svg">
4
+ <img src="https://arcjet.com/logo/arcjet-light-lockup-voyage-horizontal.svg" alt="Arcjet Logo" height="128" width="auto">
5
5
  </picture>
6
6
  </a>
7
7
 
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ArcjetDecision } from "@arcjet/protocol";
1
+ import type { ArcjetDecision } from "@arcjet/protocol";
2
2
  interface HeaderLike {
3
3
  has(name: string): boolean;
4
4
  get(name: string): string | null;
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import logger from '@arcjet/logger';
1
+ import format from '@arcjet/sprintf';
2
+ import '@arcjet/protocol';
2
3
 
3
4
  function isHeaderLike(value) {
4
5
  if ("has" in value &&
@@ -89,14 +90,14 @@ function setRateLimitHeaders(value, decision) {
89
90
  const policies = new Map();
90
91
  for (const reason of rateLimitReasons) {
91
92
  if (policies.has(reason.max)) {
92
- logger.error("Invalid rate limit policy—two policies should not share the same limit");
93
+ console.error("Invalid rate limit policy—two policies should not share the same limit");
93
94
  return;
94
95
  }
95
96
  if (typeof reason.max !== "number" ||
96
97
  typeof reason.window !== "number" ||
97
98
  typeof reason.remaining !== "number" ||
98
99
  typeof reason.reset !== "number") {
99
- logger.error("Invalid rate limit encountered: %s", reason);
100
+ console.error(format("Invalid rate limit encountered: %o", reason));
100
101
  return;
101
102
  }
102
103
  policies.set(reason.max, reason.window);
@@ -116,7 +117,7 @@ function setRateLimitHeaders(value, decision) {
116
117
  typeof decision.reason.window !== "number" ||
117
118
  typeof decision.reason.remaining !== "number" ||
118
119
  typeof decision.reason.reset !== "number") {
119
- logger.error("Invalid rate limit encountered: %s", decision.reason);
120
+ console.error(format("Invalid rate limit encountered: %o", decision.reason));
120
121
  return;
121
122
  }
122
123
  limit = toLimitString(decision.reason);
@@ -128,10 +129,10 @@ function setRateLimitHeaders(value, decision) {
128
129
  }
129
130
  if (isHeaderLike(value)) {
130
131
  if (value.has("RateLimit")) {
131
- logger.warn("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.get("RateLimit"), limit);
132
+ console.warn(format("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.get("RateLimit"), limit));
132
133
  }
133
134
  if (value.has("RateLimit-Policy")) {
134
- logger.warn("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.get("RateLimit-Policy"), limit);
135
+ console.warn(format("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.get("RateLimit-Policy"), limit));
135
136
  }
136
137
  value.set("RateLimit", limit);
137
138
  value.set("RateLimit-Policy", policy);
@@ -140,10 +141,10 @@ function setRateLimitHeaders(value, decision) {
140
141
  }
141
142
  if (isResponseLike(value)) {
142
143
  if (value.headers.has("RateLimit")) {
143
- logger.warn("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.headers.get("RateLimit"), limit);
144
+ console.warn(format("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.headers.get("RateLimit"), limit));
144
145
  }
145
146
  if (value.headers.has("RateLimit-Policy")) {
146
- logger.warn("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.headers.get("RateLimit-Policy"), limit);
147
+ console.warn(format("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.headers.get("RateLimit-Policy"), limit));
147
148
  }
148
149
  value.headers.set("RateLimit", limit);
149
150
  value.headers.set("RateLimit-Policy", policy);
@@ -152,21 +153,21 @@ function setRateLimitHeaders(value, decision) {
152
153
  }
153
154
  if (isOutgoingMessageLike(value)) {
154
155
  if (value.headersSent) {
155
- logger.error("Headers have already been sent—cannot set RateLimit header");
156
+ console.error("Headers have already been sent—cannot set RateLimit header");
156
157
  return;
157
158
  }
158
159
  if (value.hasHeader("RateLimit")) {
159
- logger.warn("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.getHeader("RateLimit"), limit);
160
+ console.warn(format("Response already contains `RateLimit` header\n Original: %s\n New: %s", value.getHeader("RateLimit"), limit));
160
161
  }
161
162
  if (value.hasHeader("RateLimit-Policy")) {
162
- logger.warn("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.getHeader("RateLimit-Policy"), limit);
163
+ console.warn(format("Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s", value.getHeader("RateLimit-Policy"), limit));
163
164
  }
164
165
  value.setHeader("RateLimit", limit);
165
166
  value.setHeader("RateLimit-Policy", policy);
166
167
  // The response was handled
167
168
  return;
168
169
  }
169
- logger.debug("Cannot determine if response is Response or OutgoingMessage type");
170
+ console.debug("Cannot determine if response is Response or OutgoingMessage type");
170
171
  }
171
172
 
172
173
  export { setRateLimitHeaders };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcjet/decorate",
3
- "version": "1.0.0-alpha.9",
3
+ "version": "1.0.0-beta.1",
4
4
  "description": "Arcjet utilities for decorating responses with information",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://arcjet.com",
@@ -29,7 +29,6 @@
29
29
  "README.md",
30
30
  "*.js",
31
31
  "*.d.ts",
32
- "*.ts",
33
32
  "!*.config.js"
34
33
  ],
35
34
  "scripts": {
@@ -37,21 +36,20 @@
37
36
  "build": "rollup --config rollup.config.js",
38
37
  "lint": "eslint .",
39
38
  "pretest": "npm run build",
40
- "test": "NODE_OPTIONS=--experimental-vm-modules jest"
39
+ "test": "node --test --experimental-test-coverage"
41
40
  },
42
41
  "dependencies": {
43
- "@arcjet/logger": "1.0.0-alpha.9",
44
- "@arcjet/protocol": "1.0.0-alpha.9"
42
+ "@arcjet/protocol": "1.0.0-beta.1",
43
+ "@arcjet/sprintf": "1.0.0-beta.1"
45
44
  },
46
45
  "devDependencies": {
47
- "@arcjet/eslint-config": "1.0.0-alpha.9",
48
- "@arcjet/rollup-config": "1.0.0-alpha.9",
49
- "@arcjet/tsconfig": "1.0.0-alpha.9",
50
- "@jest/globals": "29.7.0",
51
- "@rollup/wasm-node": "4.12.0",
46
+ "@arcjet/eslint-config": "1.0.0-beta.1",
47
+ "@arcjet/rollup-config": "1.0.0-beta.1",
48
+ "@arcjet/tsconfig": "1.0.0-beta.1",
49
+ "@rollup/wasm-node": "4.30.1",
52
50
  "@types/node": "18.18.0",
53
- "jest": "29.7.0",
54
- "typescript": "5.3.3"
51
+ "expect": "29.7.0",
52
+ "typescript": "5.7.3"
55
53
  },
56
54
  "publishConfig": {
57
55
  "access": "public",
package/index.ts DELETED
@@ -1,312 +0,0 @@
1
- import logger from "@arcjet/logger";
2
- import {
3
- ArcjetDecision,
4
- ArcjetRateLimitReason,
5
- ArcjetReason,
6
- ArcjetRuleResult,
7
- } from "@arcjet/protocol";
8
-
9
- interface HeaderLike {
10
- has(name: string): boolean;
11
- get(name: string): string | null;
12
- set(name: string, value: string): void;
13
- }
14
-
15
- interface ResponseLike {
16
- // If this is defined, we can expect to be working with a `Response` or
17
- // `NextResponse`.
18
- headers: HeaderLike;
19
- }
20
-
21
- interface OutgoingMessageLike {
22
- headersSent: boolean;
23
- hasHeader: (name: string) => boolean;
24
- getHeader: (name: string) => number | string | string[] | undefined;
25
- setHeader: (
26
- name: string,
27
- value: number | string | ReadonlyArray<string>,
28
- ) => unknown;
29
- }
30
-
31
- export interface ArcjetCanDecorate {
32
- // If these are defined, we can expect to be working with `Headers` directly
33
- has?: (name: string) => boolean;
34
- get?: (name: string) => string | null;
35
- set?: (name: string, value: string) => void;
36
-
37
- // If this is defined, we can expect to be working with a `Response` or
38
- // `NextResponse`.
39
- headers?: HeaderLike;
40
-
41
- // Otherwise, we'll be working with an `http.OutgoingMessage` and we'll need
42
- // to use these values.
43
- headersSent?: boolean;
44
- hasHeader?: (name: string) => boolean;
45
- getHeader?: (name: string) => number | string | string[] | undefined;
46
- setHeader?: (
47
- name: string,
48
- value: number | string | ReadonlyArray<string>,
49
- ) => unknown;
50
- }
51
-
52
- function isHeaderLike(value: ArcjetCanDecorate): value is HeaderLike {
53
- if (
54
- "has" in value &&
55
- typeof value.has === "function" &&
56
- "get" in value &&
57
- typeof value.get === "function" &&
58
- "set" in value &&
59
- typeof value.set === "function"
60
- ) {
61
- return true;
62
- }
63
-
64
- return false;
65
- }
66
-
67
- function isResponseLike(value: ArcjetCanDecorate): value is ResponseLike {
68
- if (typeof value.headers === "undefined") {
69
- return false;
70
- }
71
-
72
- return isHeaderLike(value.headers);
73
- }
74
-
75
- function isOutgoingMessageLike(
76
- response: ArcjetCanDecorate,
77
- ): response is OutgoingMessageLike {
78
- if (typeof response.headersSent !== "boolean") {
79
- return false;
80
- }
81
-
82
- if (typeof response.hasHeader !== "function") {
83
- return false;
84
- }
85
-
86
- if (typeof response.getHeader !== "function") {
87
- return false;
88
- }
89
-
90
- if (typeof response.setHeader !== "function") {
91
- return false;
92
- }
93
-
94
- return true;
95
- }
96
-
97
- function sortByLowestMax(
98
- [maxA, _windowA]: [number, number],
99
- [maxB, _windowB]: [number, number],
100
- ) {
101
- return maxA - maxB;
102
- }
103
-
104
- function toPolicyString([max, window]: [number, number]) {
105
- return `${max};w=${window}`;
106
- }
107
-
108
- function toLimitString({
109
- max,
110
- remaining,
111
- reset,
112
- }: {
113
- max: number;
114
- remaining: number;
115
- reset: number;
116
- }) {
117
- return `limit=${max}, remaining=${remaining}, reset=${reset}`;
118
- }
119
-
120
- function extractReason(result: ArcjetRuleResult): ArcjetReason {
121
- return result.reason;
122
- }
123
-
124
- function isRateLimitReason(
125
- reason: ArcjetReason,
126
- ): reason is ArcjetRateLimitReason {
127
- return reason.isRateLimit();
128
- }
129
-
130
- function nearestLimit(
131
- current: ArcjetRateLimitReason,
132
- next: ArcjetRateLimitReason,
133
- ) {
134
- if (current.remaining < next.remaining) {
135
- return current;
136
- }
137
-
138
- if (current.remaining > next.remaining) {
139
- return next;
140
- }
141
-
142
- // Reaching here means `remaining` is equal so prioritize closest reset
143
- if (current.reset < next.reset) {
144
- return current;
145
- }
146
-
147
- if (current.reset > next.reset) {
148
- return next;
149
- }
150
-
151
- // Reaching here means that `remaining` and `reset` are equal, so prioritize
152
- // the smallest `max`
153
- if (current.max < next.max) {
154
- return current;
155
- }
156
-
157
- // All else equal, just return the next item in the list
158
- return next;
159
- }
160
-
161
- /**
162
- * Decorates an object with `RateLimit` and `RateLimit-Policy` headers based
163
- * on an {@link ArcjetDecision} and conforming to the [Rate Limit fields for
164
- * HTTP](https://ietf-wg-httpapi.github.io/ratelimit-headers/draft-ietf-httpapi-ratelimit-headers.html)
165
- * draft specification.
166
- *
167
- * @param value The object to decorate—must be similar to {@link Headers}, {@link Response} or
168
- * {@link OutgoingMessage}.
169
- * @param decision The {@link ArcjetDecision} that was made by calling `protect()` on the SDK.
170
- */
171
- export function setRateLimitHeaders(
172
- value: ArcjetCanDecorate,
173
- decision: ArcjetDecision,
174
- ) {
175
- const rateLimitReasons = decision.results
176
- .map(extractReason)
177
- .filter(isRateLimitReason);
178
-
179
- let policy: string;
180
- let limit: string;
181
- if (rateLimitReasons.length > 0) {
182
- const policies = new Map<number, number>();
183
- for (const reason of rateLimitReasons) {
184
- if (policies.has(reason.max)) {
185
- logger.error(
186
- "Invalid rate limit policy—two policies should not share the same limit",
187
- );
188
- return;
189
- }
190
-
191
- if (
192
- typeof reason.max !== "number" ||
193
- typeof reason.window !== "number" ||
194
- typeof reason.remaining !== "number" ||
195
- typeof reason.reset !== "number"
196
- ) {
197
- logger.error("Invalid rate limit encountered: %s", reason);
198
- return;
199
- }
200
-
201
- policies.set(reason.max, reason.window);
202
- }
203
-
204
- const rl = rateLimitReasons.reduce(nearestLimit);
205
-
206
- limit = toLimitString(rl);
207
- policy = Array.from(policies.entries())
208
- .sort(sortByLowestMax)
209
- .map(toPolicyString)
210
- .join(", ");
211
- } else {
212
- // For cached decisions, we may not have rule results, but we'd still have
213
- // the top-level reason.
214
- if (isRateLimitReason(decision.reason)) {
215
- if (
216
- typeof decision.reason.max !== "number" ||
217
- typeof decision.reason.window !== "number" ||
218
- typeof decision.reason.remaining !== "number" ||
219
- typeof decision.reason.reset !== "number"
220
- ) {
221
- logger.error("Invalid rate limit encountered: %s", decision.reason);
222
- return;
223
- }
224
-
225
- limit = toLimitString(decision.reason);
226
- policy = toPolicyString([decision.reason.max, decision.reason.window]);
227
- } else {
228
- return;
229
- }
230
- }
231
-
232
- if (isHeaderLike(value)) {
233
- if (value.has("RateLimit")) {
234
- logger.warn(
235
- "Response already contains `RateLimit` header\n Original: %s\n New: %s",
236
- value.get("RateLimit"),
237
- limit,
238
- );
239
- }
240
- if (value.has("RateLimit-Policy")) {
241
- logger.warn(
242
- "Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s",
243
- value.get("RateLimit-Policy"),
244
- limit,
245
- );
246
- }
247
-
248
- value.set("RateLimit", limit);
249
- value.set("RateLimit-Policy", policy);
250
-
251
- // The response was handled
252
- return;
253
- }
254
-
255
- if (isResponseLike(value)) {
256
- if (value.headers.has("RateLimit")) {
257
- logger.warn(
258
- "Response already contains `RateLimit` header\n Original: %s\n New: %s",
259
- value.headers.get("RateLimit"),
260
- limit,
261
- );
262
- }
263
- if (value.headers.has("RateLimit-Policy")) {
264
- logger.warn(
265
- "Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s",
266
- value.headers.get("RateLimit-Policy"),
267
- limit,
268
- );
269
- }
270
-
271
- value.headers.set("RateLimit", limit);
272
- value.headers.set("RateLimit-Policy", policy);
273
-
274
- // The response was handled
275
- return;
276
- }
277
-
278
- if (isOutgoingMessageLike(value)) {
279
- if (value.headersSent) {
280
- logger.error(
281
- "Headers have already been sent—cannot set RateLimit header",
282
- );
283
- return;
284
- }
285
-
286
- if (value.hasHeader("RateLimit")) {
287
- logger.warn(
288
- "Response already contains `RateLimit` header\n Original: %s\n New: %s",
289
- value.getHeader("RateLimit"),
290
- limit,
291
- );
292
- }
293
-
294
- if (value.hasHeader("RateLimit-Policy")) {
295
- logger.warn(
296
- "Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s",
297
- value.getHeader("RateLimit-Policy"),
298
- limit,
299
- );
300
- }
301
-
302
- value.setHeader("RateLimit", limit);
303
- value.setHeader("RateLimit-Policy", policy);
304
-
305
- // The response was handled
306
- return;
307
- }
308
-
309
- logger.debug(
310
- "Cannot determine if response is Response or OutgoingMessage type",
311
- );
312
- }