@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.
- package/package.json +8 -9
- 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.
|
|
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.
|
|
44
|
-
"@arcjet/sprintf": "1.0.0-alpha.
|
|
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.
|
|
48
|
-
"@arcjet/rollup-config": "1.0.0-alpha.
|
|
49
|
-
"@arcjet/tsconfig": "1.0.0-alpha.
|
|
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.
|
|
50
|
+
"@rollup/wasm-node": "4.27.4",
|
|
52
51
|
"@types/node": "18.18.0",
|
|
53
52
|
"jest": "29.7.0",
|
|
54
|
-
"typescript": "5.
|
|
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
|
-
}
|