@arcjet/decorate 1.0.0-alpha.31 → 1.0.0-alpha.32

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 (2) hide show
  1. package/package.json +8 -9
  2. package/index.ts +0 -326
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcjet/decorate",
3
- "version": "1.0.0-alpha.31",
3
+ "version": "1.0.0-alpha.32",
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": {
@@ -40,18 +39,18 @@
40
39
  "test": "NODE_OPTIONS=--experimental-vm-modules jest"
41
40
  },
42
41
  "dependencies": {
43
- "@arcjet/protocol": "1.0.0-alpha.31",
44
- "@arcjet/sprintf": "1.0.0-alpha.31"
42
+ "@arcjet/protocol": "1.0.0-alpha.32",
43
+ "@arcjet/sprintf": "1.0.0-alpha.32"
45
44
  },
46
45
  "devDependencies": {
47
- "@arcjet/eslint-config": "1.0.0-alpha.31",
48
- "@arcjet/rollup-config": "1.0.0-alpha.31",
49
- "@arcjet/tsconfig": "1.0.0-alpha.31",
46
+ "@arcjet/eslint-config": "1.0.0-alpha.32",
47
+ "@arcjet/rollup-config": "1.0.0-alpha.32",
48
+ "@arcjet/tsconfig": "1.0.0-alpha.32",
50
49
  "@jest/globals": "29.7.0",
51
- "@rollup/wasm-node": "4.27.3",
50
+ "@rollup/wasm-node": "4.27.4",
52
51
  "@types/node": "18.18.0",
53
52
  "jest": "29.7.0",
54
- "typescript": "5.6.3"
53
+ "typescript": "5.7.2"
55
54
  },
56
55
  "publishConfig": {
57
56
  "access": "public",
package/index.ts DELETED
@@ -1,326 +0,0 @@
1
- import format from "@arcjet/sprintf";
2
- import type { ArcjetDecision } from "@arcjet/protocol";
3
- import {
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
- console.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
- console.error(format("Invalid rate limit encountered: %o", 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
- console.error(
222
- format("Invalid rate limit encountered: %o", decision.reason),
223
- );
224
- return;
225
- }
226
-
227
- limit = toLimitString(decision.reason);
228
- policy = toPolicyString([decision.reason.max, decision.reason.window]);
229
- } else {
230
- return;
231
- }
232
- }
233
-
234
- if (isHeaderLike(value)) {
235
- if (value.has("RateLimit")) {
236
- console.warn(
237
- format(
238
- "Response already contains `RateLimit` header\n Original: %s\n New: %s",
239
- value.get("RateLimit"),
240
- limit,
241
- ),
242
- );
243
- }
244
- if (value.has("RateLimit-Policy")) {
245
- console.warn(
246
- format(
247
- "Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s",
248
- value.get("RateLimit-Policy"),
249
- limit,
250
- ),
251
- );
252
- }
253
-
254
- value.set("RateLimit", limit);
255
- value.set("RateLimit-Policy", policy);
256
-
257
- // The response was handled
258
- return;
259
- }
260
-
261
- if (isResponseLike(value)) {
262
- if (value.headers.has("RateLimit")) {
263
- console.warn(
264
- format(
265
- "Response already contains `RateLimit` header\n Original: %s\n New: %s",
266
- value.headers.get("RateLimit"),
267
- limit,
268
- ),
269
- );
270
- }
271
- if (value.headers.has("RateLimit-Policy")) {
272
- console.warn(
273
- format(
274
- "Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s",
275
- value.headers.get("RateLimit-Policy"),
276
- limit,
277
- ),
278
- );
279
- }
280
-
281
- value.headers.set("RateLimit", limit);
282
- value.headers.set("RateLimit-Policy", policy);
283
-
284
- // The response was handled
285
- return;
286
- }
287
-
288
- if (isOutgoingMessageLike(value)) {
289
- if (value.headersSent) {
290
- console.error(
291
- "Headers have already been sent—cannot set RateLimit header",
292
- );
293
- return;
294
- }
295
-
296
- if (value.hasHeader("RateLimit")) {
297
- console.warn(
298
- format(
299
- "Response already contains `RateLimit` header\n Original: %s\n New: %s",
300
- value.getHeader("RateLimit"),
301
- limit,
302
- ),
303
- );
304
- }
305
-
306
- if (value.hasHeader("RateLimit-Policy")) {
307
- console.warn(
308
- format(
309
- "Response already contains `RateLimit-Policy` header\n Original: %s\n New: %s",
310
- value.getHeader("RateLimit-Policy"),
311
- limit,
312
- ),
313
- );
314
- }
315
-
316
- value.setHeader("RateLimit", limit);
317
- value.setHeader("RateLimit-Policy", policy);
318
-
319
- // The response was handled
320
- return;
321
- }
322
-
323
- console.debug(
324
- "Cannot determine if response is Response or OutgoingMessage type",
325
- );
326
- }