@arcjet/bun 1.3.0 → 1.4.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/README.md +485 -57
- package/index.js +1 -1
- package/package.json +25 -18
package/README.md
CHANGED
|
@@ -16,95 +16,522 @@
|
|
|
16
16
|
</a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
-
[Arcjet][arcjet]
|
|
20
|
-
code. Implement rate limiting, bot protection, email verification, and defense
|
|
21
|
-
against common attacks.
|
|
19
|
+
[Arcjet][arcjet] is the runtime security platform that ships with your AI code. Stop bots and automated attacks from burning your AI budget, leaking data, or misusing tools with Arcjet's AI security building blocks. Every feature works with any Bun application.
|
|
22
20
|
|
|
23
|
-
This is the [Arcjet][arcjet] SDK for [Bun
|
|
24
|
-
|
|
25
|
-
- [npm package (`@arcjet/bun`)](https://www.npmjs.com/package/@arcjet/bun)
|
|
26
|
-
- [GitHub source code (`arcjet-bun/` in `arcjet/arcjet-js`)](https://github.com/arcjet/arcjet-js/tree/main/arcjet-bun)
|
|
21
|
+
This is the [Arcjet][arcjet] SDK for [Bun][bun-sh].
|
|
27
22
|
|
|
28
23
|
## Getting started
|
|
29
24
|
|
|
30
|
-
|
|
25
|
+
1. Get your API key at [`app.arcjet.com`](https://app.arcjet.com)
|
|
26
|
+
2. `bun add @arcjet/bun`
|
|
27
|
+
3. Set `ARCJET_KEY=ajkey_yourkey` in your environment
|
|
28
|
+
4. Add Arcjet to your server — see the [quick start](#quick-start) below
|
|
29
|
+
|
|
30
|
+
[npm package](https://www.npmjs.com/package/@arcjet/bun) |
|
|
31
|
+
[GitHub source](https://github.com/arcjet/arcjet-js/tree/main/arcjet-bun) |
|
|
32
|
+
[Full docs][arcjet-reference-bun] |
|
|
33
|
+
[Other SDKs on GitHub](https://github.com/arcjet)
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- 🔒 [Prompt Injection Detection](#prompt-injection-detection) — detect and block
|
|
38
|
+
prompt injection attacks before they reach your LLM.
|
|
39
|
+
- 🤖 [Bot Protection](#bot-protection) — stop scrapers, credential stuffers, and
|
|
40
|
+
AI crawlers from abusing your endpoints.
|
|
41
|
+
- 🛑 [Rate Limiting](#rate-limiting) — token bucket, fixed window, and sliding
|
|
42
|
+
window algorithms; model AI token budgets per user.
|
|
43
|
+
- 🕵️ [Sensitive Information Detection](#sensitive-information-detection) — block
|
|
44
|
+
PII, credit cards, and custom patterns from entering your AI pipeline.
|
|
45
|
+
- 🛡️ [Shield WAF](#shield-waf) — protect against SQL injection, XSS, and other
|
|
46
|
+
common web attacks.
|
|
47
|
+
- 📧 [Email Validation](#email-validation) — block disposable, invalid, and
|
|
48
|
+
undeliverable addresses at signup.
|
|
49
|
+
- 📝 [Signup Form Protection][signup-protection-docs] — combines bot protection,
|
|
50
|
+
email validation, and rate limiting to protect your signup forms.
|
|
51
|
+
- 🎯 [Request Filters](#request-filters) — expression-based rules on IP, path,
|
|
52
|
+
headers, and custom fields.
|
|
53
|
+
- 🌐 [IP Analysis](#ip-analysis) — geolocation, ASN, VPN, proxy, Tor, and hosting
|
|
54
|
+
detection included with every request.
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
This example protects a Bun HTTP server with bot detection, Shield WAF, and
|
|
59
|
+
token bucket rate limiting.
|
|
31
60
|
|
|
32
|
-
|
|
61
|
+
```ts
|
|
62
|
+
// index.ts
|
|
63
|
+
import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/bun";
|
|
64
|
+
import { isSpoofedBot } from "@arcjet/inspect";
|
|
65
|
+
import { env } from "bun";
|
|
33
66
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
const aj = arcjet({
|
|
68
|
+
key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
|
|
69
|
+
rules: [
|
|
70
|
+
// Shield protects your app from common attacks e.g. SQL injection
|
|
71
|
+
shield({ mode: "LIVE" }),
|
|
72
|
+
// Create a bot detection rule
|
|
73
|
+
detectBot({
|
|
74
|
+
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
|
|
75
|
+
// Block all bots except the following
|
|
76
|
+
allow: [
|
|
77
|
+
"CATEGORY:SEARCH_ENGINE", // Google, Bing, etc
|
|
78
|
+
// Uncomment to allow these other common bot categories
|
|
79
|
+
// See the full list at https://arcjet.com/bot-list
|
|
80
|
+
//"CATEGORY:MONITOR", // Uptime monitoring services
|
|
81
|
+
//"CATEGORY:PREVIEW", // Link previews e.g. Slack, Discord
|
|
82
|
+
],
|
|
83
|
+
}),
|
|
84
|
+
// Token bucket rate limit. Other algorithms are supported.
|
|
85
|
+
tokenBucket({
|
|
86
|
+
mode: "LIVE",
|
|
87
|
+
// Tracked by IP address by default, but this can be customized
|
|
88
|
+
// See https://docs.arcjet.com/fingerprints
|
|
89
|
+
refillRate: 5, // Refill 5 tokens per interval
|
|
90
|
+
interval: 10, // Refill every 10 seconds
|
|
91
|
+
capacity: 10, // Bucket capacity of 10 tokens
|
|
92
|
+
}),
|
|
93
|
+
],
|
|
94
|
+
});
|
|
37
95
|
|
|
38
|
-
|
|
96
|
+
export default {
|
|
97
|
+
port: 3000,
|
|
98
|
+
fetch: aj.handler(async (req) => {
|
|
99
|
+
const decision = await aj.protect(req, { requested: 5 }); // Deduct 5 tokens
|
|
100
|
+
console.log("Arcjet decision", decision);
|
|
39
101
|
|
|
40
|
-
|
|
102
|
+
if (decision.isDenied()) {
|
|
103
|
+
if (decision.reason.isRateLimit()) {
|
|
104
|
+
return new Response("Too many requests", { status: 429 });
|
|
105
|
+
} else if (decision.reason.isBot()) {
|
|
106
|
+
return new Response("No bots allowed", { status: 403 });
|
|
107
|
+
} else {
|
|
108
|
+
return new Response("Forbidden", { status: 403 });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
41
111
|
|
|
42
|
-
|
|
112
|
+
// Requests from hosting IPs are likely from bots.
|
|
113
|
+
// https://docs.arcjet.com/blueprints/vpn-proxy-detection
|
|
114
|
+
if (decision.ip.isHosting()) {
|
|
115
|
+
return new Response("Forbidden", { status: 403 });
|
|
116
|
+
}
|
|
43
117
|
|
|
44
|
-
|
|
118
|
+
// Verifies the authenticity of common bots using IP data.
|
|
119
|
+
// Verification isn't always possible, so check the results separately.
|
|
120
|
+
// https://docs.arcjet.com/bot-protection/reference#bot-verification
|
|
121
|
+
if (decision.results.some(isSpoofedBot)) {
|
|
122
|
+
return new Response("Forbidden", { status: 403 });
|
|
123
|
+
}
|
|
45
124
|
|
|
46
|
-
|
|
125
|
+
return new Response("Hello world");
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
```
|
|
47
129
|
|
|
48
|
-
|
|
130
|
+
For the full reference, see the [Arcjet Bun SDK docs][arcjet-reference-bun].
|
|
49
131
|
|
|
50
|
-
|
|
51
|
-
Arcjet helps you secure your Bun server.
|
|
52
|
-
This package exists so that we can provide the best possible experience to
|
|
53
|
-
Bun users.
|
|
132
|
+
## Prompt injection detection
|
|
54
133
|
|
|
55
|
-
|
|
134
|
+
Detect and block prompt injection attacks — attempts to override your AI
|
|
135
|
+
model's instructions — before they reach your model. Pass the user's message
|
|
136
|
+
via `detectPromptInjectionMessage` on each `protect()` call.
|
|
56
137
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
138
|
+
```ts
|
|
139
|
+
import arcjet, { detectPromptInjection } from "@arcjet/bun";
|
|
140
|
+
import { env } from "bun";
|
|
60
141
|
|
|
61
|
-
|
|
142
|
+
const aj = arcjet({
|
|
143
|
+
key: env.ARCJET_KEY!,
|
|
144
|
+
rules: [
|
|
145
|
+
detectPromptInjection({
|
|
146
|
+
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
|
|
147
|
+
}),
|
|
148
|
+
],
|
|
149
|
+
});
|
|
62
150
|
|
|
63
|
-
|
|
64
|
-
|
|
151
|
+
export default {
|
|
152
|
+
fetch: aj.handler(async (req) => {
|
|
153
|
+
const { message } = await req.json();
|
|
154
|
+
|
|
155
|
+
const decision = await aj.protect(req, {
|
|
156
|
+
detectPromptInjectionMessage: message,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (decision.isDenied() && decision.reason.isPromptInjection()) {
|
|
160
|
+
return new Response(
|
|
161
|
+
"Prompt injection detected — please rephrase your message",
|
|
162
|
+
{ status: 400 },
|
|
163
|
+
);
|
|
164
|
+
}
|
|
65
165
|
|
|
66
|
-
|
|
67
|
-
|
|
166
|
+
// Forward to your AI model...
|
|
167
|
+
return new Response("OK");
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
68
170
|
```
|
|
69
171
|
|
|
70
|
-
##
|
|
172
|
+
## Bot protection
|
|
71
173
|
|
|
72
|
-
|
|
73
|
-
|
|
174
|
+
Arcjet allows you to configure a list of bots to allow or deny. Specifying
|
|
175
|
+
`allow` means all other bots are denied. An empty allow list blocks all bots.
|
|
74
176
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
177
|
+
Available categories: `CATEGORY:ACADEMIC`, `CATEGORY:ADVERTISING`,
|
|
178
|
+
`CATEGORY:AI`, `CATEGORY:AMAZON`, `CATEGORY:APPLE`, `CATEGORY:ARCHIVE`,
|
|
179
|
+
`CATEGORY:BOTNET`, `CATEGORY:FEEDFETCHER`, `CATEGORY:GOOGLE`,
|
|
180
|
+
`CATEGORY:META`, `CATEGORY:MICROSOFT`, `CATEGORY:MONITOR`,
|
|
181
|
+
`CATEGORY:OPTIMIZER`, `CATEGORY:PREVIEW`, `CATEGORY:PROGRAMMATIC`,
|
|
182
|
+
`CATEGORY:SEARCH_ENGINE`, `CATEGORY:SLACK`, `CATEGORY:SOCIAL`,
|
|
183
|
+
`CATEGORY:TOOL`, `CATEGORY:UNKNOWN`, `CATEGORY:VERCEL`,
|
|
184
|
+
`CATEGORY:WEBHOOK`, `CATEGORY:YAHOO`. You can also allow or deny
|
|
185
|
+
[specific bots by name][bot-list].
|
|
78
186
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
187
|
+
```ts
|
|
188
|
+
import arcjet, { detectBot } from "@arcjet/bun";
|
|
189
|
+
import { isSpoofedBot } from "@arcjet/inspect";
|
|
190
|
+
import { env } from "bun";
|
|
82
191
|
|
|
83
192
|
const aj = arcjet({
|
|
84
|
-
key:
|
|
193
|
+
key: env.ARCJET_KEY!,
|
|
85
194
|
rules: [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
195
|
+
detectBot({
|
|
196
|
+
mode: "LIVE",
|
|
197
|
+
allow: [
|
|
198
|
+
"CATEGORY:SEARCH_ENGINE",
|
|
199
|
+
// See the full list at https://arcjet.com/bot-list
|
|
200
|
+
],
|
|
201
|
+
}),
|
|
89
202
|
],
|
|
90
203
|
});
|
|
91
204
|
|
|
92
205
|
export default {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const decision = await aj.protect(request);
|
|
206
|
+
fetch: aj.handler(async (req) => {
|
|
207
|
+
const decision = await aj.protect(req);
|
|
96
208
|
|
|
97
|
-
if (decision.isDenied()) {
|
|
98
|
-
return Response
|
|
209
|
+
if (decision.isDenied() && decision.reason.isBot()) {
|
|
210
|
+
return new Response("No bots allowed", { status: 403 });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Verifies the authenticity of common bots using IP data.
|
|
214
|
+
if (decision.results.some(isSpoofedBot)) {
|
|
215
|
+
return new Response("Forbidden", { status: 403 });
|
|
99
216
|
}
|
|
100
217
|
|
|
101
|
-
return Response
|
|
218
|
+
return new Response("Hello world");
|
|
102
219
|
}),
|
|
103
220
|
};
|
|
104
221
|
```
|
|
105
222
|
|
|
106
|
-
|
|
107
|
-
|
|
223
|
+
### Bot categories
|
|
224
|
+
|
|
225
|
+
Bots can be configured by [category][bot-categories-docs] and/or by [specific
|
|
226
|
+
bot name][bot-list]. For example, to allow search engines and the OpenAI
|
|
227
|
+
crawler, but deny all other bots:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
detectBot({
|
|
231
|
+
mode: "LIVE",
|
|
232
|
+
allow: ["CATEGORY:SEARCH_ENGINE", "OPENAI_CRAWLER_SEARCH"],
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Verified vs spoofed bots
|
|
237
|
+
|
|
238
|
+
Bots claiming to be well-known crawlers (e.g. Googlebot) are verified by
|
|
239
|
+
checking their IP address against known IP ranges. If a bot fails verification,
|
|
240
|
+
it is labeled as spoofed. Use `isSpoofedBot` from `@arcjet/inspect` to check:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { isSpoofedBot } from "@arcjet/inspect";
|
|
244
|
+
|
|
245
|
+
if (decision.results.some(isSpoofedBot)) {
|
|
246
|
+
return new Response("Forbidden", { status: 403 });
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Rate limiting
|
|
251
|
+
|
|
252
|
+
Arcjet supports token bucket, fixed window, and sliding window algorithms.
|
|
253
|
+
Token buckets are ideal for controlling AI token budgets — set `capacity` to
|
|
254
|
+
the max tokens a user can spend, `refillRate` to how many tokens are restored
|
|
255
|
+
per `interval`, and deduct tokens per request via `requested` in `protect()`.
|
|
256
|
+
The `interval` accepts strings (`"1s"`, `"1m"`, `"1h"`, `"1d"`) or seconds as
|
|
257
|
+
a number. Use `characteristics` to track limits per user instead of per IP.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import arcjet, { tokenBucket } from "@arcjet/bun";
|
|
261
|
+
import { env } from "bun";
|
|
262
|
+
|
|
263
|
+
const aj = arcjet({
|
|
264
|
+
key: env.ARCJET_KEY!,
|
|
265
|
+
characteristics: ["userId"], // Track per user
|
|
266
|
+
rules: [
|
|
267
|
+
tokenBucket({
|
|
268
|
+
mode: "LIVE",
|
|
269
|
+
refillRate: 2_000, // Refill 2,000 tokens per hour
|
|
270
|
+
interval: "1h",
|
|
271
|
+
capacity: 5_000, // Maximum 5,000 tokens in the bucket
|
|
272
|
+
}),
|
|
273
|
+
],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const decision = await aj.protect(req, {
|
|
277
|
+
userId: "user-123",
|
|
278
|
+
requested: estimate, // Number of tokens to deduct
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (decision.isDenied() && decision.reason.isRateLimit()) {
|
|
282
|
+
return new Response("Rate limit exceeded", { status: 429 });
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Sensitive information detection
|
|
287
|
+
|
|
288
|
+
Detect and block PII in request content. Pass the content to scan via
|
|
289
|
+
`sensitiveInfoValue` on each `protect()` call. Built-in entity types:
|
|
290
|
+
`CREDIT_CARD_NUMBER`, `EMAIL`, `PHONE_NUMBER`, `IP_ADDRESS`. You can also
|
|
291
|
+
provide a custom `detect` callback for additional patterns.
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
import arcjet, { sensitiveInfo } from "@arcjet/bun";
|
|
295
|
+
import { env } from "bun";
|
|
296
|
+
|
|
297
|
+
const aj = arcjet({
|
|
298
|
+
key: env.ARCJET_KEY!,
|
|
299
|
+
rules: [
|
|
300
|
+
sensitiveInfo({
|
|
301
|
+
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
|
|
302
|
+
deny: ["CREDIT_CARD_NUMBER", "EMAIL", "PHONE_NUMBER"],
|
|
303
|
+
}),
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const decision = await aj.protect(req, {
|
|
308
|
+
sensitiveInfoValue: userMessage, // The text content to scan
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (decision.isDenied() && decision.reason.isSensitiveInfo()) {
|
|
312
|
+
return new Response("Sensitive information detected", { status: 400 });
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Shield WAF
|
|
317
|
+
|
|
318
|
+
Protect your application against common web attacks, including the OWASP
|
|
319
|
+
Top 10.
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import arcjet, { shield } from "@arcjet/bun";
|
|
323
|
+
import { env } from "bun";
|
|
324
|
+
|
|
325
|
+
const aj = arcjet({
|
|
326
|
+
key: env.ARCJET_KEY!,
|
|
327
|
+
rules: [
|
|
328
|
+
shield({
|
|
329
|
+
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
|
|
330
|
+
}),
|
|
331
|
+
],
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Email validation
|
|
336
|
+
|
|
337
|
+
Validate and verify email addresses. Deny types: `DISPOSABLE`, `FREE`,
|
|
338
|
+
`NO_MX_RECORDS`, `NO_GRAVATAR`, `INVALID`.
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
import arcjet, { validateEmail } from "@arcjet/bun";
|
|
342
|
+
import { env } from "bun";
|
|
343
|
+
|
|
344
|
+
const aj = arcjet({
|
|
345
|
+
key: env.ARCJET_KEY!,
|
|
346
|
+
rules: [
|
|
347
|
+
validateEmail({
|
|
348
|
+
mode: "LIVE",
|
|
349
|
+
deny: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"],
|
|
350
|
+
}),
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const decision = await aj.protect(req, {
|
|
355
|
+
email: "user@example.com",
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (decision.isDenied() && decision.reason.isEmail()) {
|
|
359
|
+
return new Response("Invalid email address", { status: 400 });
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Request filters
|
|
364
|
+
|
|
365
|
+
Filter requests using expression-based rules against request properties (IP,
|
|
366
|
+
headers, path, method, etc.).
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import arcjet, { filter } from "@arcjet/bun";
|
|
370
|
+
import { env } from "bun";
|
|
371
|
+
|
|
372
|
+
const aj = arcjet({
|
|
373
|
+
key: env.ARCJET_KEY!,
|
|
374
|
+
rules: [
|
|
375
|
+
filter({
|
|
376
|
+
mode: "LIVE",
|
|
377
|
+
deny: ['ip.src == "1.2.3.4"'],
|
|
378
|
+
}),
|
|
379
|
+
],
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Block by country
|
|
384
|
+
|
|
385
|
+
Restrict access to specific countries — useful for licensing, compliance, or
|
|
386
|
+
regional rollouts. The `allow` list denies all countries not listed:
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
filter({
|
|
390
|
+
mode: "LIVE",
|
|
391
|
+
// Allow only US traffic — all other countries are denied
|
|
392
|
+
allow: ['ip.src.country == "US"'],
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Block VPN and proxy traffic
|
|
397
|
+
|
|
398
|
+
Prevent anonymized traffic from accessing sensitive endpoints — useful for
|
|
399
|
+
fraud prevention, enforcing geo-restrictions, and reducing abuse:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
filter({
|
|
403
|
+
mode: "LIVE",
|
|
404
|
+
deny: [
|
|
405
|
+
"ip.src.vpn", // VPN services
|
|
406
|
+
"ip.src.proxy", // Open proxies
|
|
407
|
+
"ip.src.tor", // Tor exit nodes
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
For more nuanced handling, use `decision.ip` helpers after calling `protect()`:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
const decision = await aj.protect(req);
|
|
416
|
+
|
|
417
|
+
if (decision.ip.isVpn() || decision.ip.isTor()) {
|
|
418
|
+
return new Response("VPN traffic not allowed", { status: 403 });
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
See the [Request Filters docs][filters-docs],
|
|
423
|
+
[IP Geolocation blueprint](https://docs.arcjet.com/blueprints/ip-geolocation), and
|
|
424
|
+
[VPN/Proxy Detection blueprint](https://docs.arcjet.com/blueprints/vpn-proxy-detection)
|
|
425
|
+
for more details.
|
|
426
|
+
|
|
427
|
+
## IP analysis
|
|
428
|
+
|
|
429
|
+
Arcjet enriches every request with IP metadata. Use these helpers to make
|
|
430
|
+
policy decisions based on network signals:
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
const decision = await aj.protect(req);
|
|
434
|
+
|
|
435
|
+
if (decision.ip.isHosting()) {
|
|
436
|
+
// Requests from cloud/hosting providers are often automated.
|
|
437
|
+
// https://docs.arcjet.com/blueprints/vpn-proxy-detection
|
|
438
|
+
return new Response("Forbidden", { status: 403 });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (decision.ip.isVpn() || decision.ip.isProxy() || decision.ip.isTor()) {
|
|
442
|
+
// Handle VPN/proxy traffic according to your policy
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Access geolocation and network details
|
|
446
|
+
console.log(decision.ip.country, decision.ip.city, decision.ip.asn);
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
## Custom characteristics
|
|
450
|
+
|
|
451
|
+
Track and limit requests by any stable identifier — user ID, API key, session,
|
|
452
|
+
etc. — rather than IP address alone.
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
const aj = arcjet({
|
|
456
|
+
key: env.ARCJET_KEY!,
|
|
457
|
+
characteristics: ["userId"], // Declare at the SDK level
|
|
458
|
+
rules: [
|
|
459
|
+
tokenBucket({
|
|
460
|
+
mode: "LIVE",
|
|
461
|
+
refillRate: 2_000,
|
|
462
|
+
interval: "1h",
|
|
463
|
+
capacity: 5_000,
|
|
464
|
+
}),
|
|
465
|
+
],
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Pass the characteristic value at request time
|
|
469
|
+
const decision = await aj.protect(req, {
|
|
470
|
+
userId: "user-123", // Replace with your actual user ID
|
|
471
|
+
requested: estimate,
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## Best practices
|
|
476
|
+
|
|
477
|
+
See the [Arcjet best practices][best-practices] for detailed guidance. Key
|
|
478
|
+
recommendations:
|
|
479
|
+
|
|
480
|
+
**Create a single client instance** and reuse it with `withRule()` for
|
|
481
|
+
route-specific rules. The SDK caches decisions and configuration, so creating a
|
|
482
|
+
new instance per request wastes that work.
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
// lib/arcjet.ts — create once, import everywhere
|
|
486
|
+
import arcjet, { shield } from "@arcjet/bun";
|
|
487
|
+
import { env } from "bun";
|
|
488
|
+
|
|
489
|
+
export default arcjet({
|
|
490
|
+
key: env.ARCJET_KEY!,
|
|
491
|
+
rules: [
|
|
492
|
+
shield({ mode: "LIVE" }), // base rules applied to every request
|
|
493
|
+
],
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
// routes/chat.ts — extend per-route with withRule()
|
|
499
|
+
import aj from "./lib/arcjet.js";
|
|
500
|
+
import { detectBot, tokenBucket } from "@arcjet/bun";
|
|
501
|
+
|
|
502
|
+
const routeAj = aj.withRule(detectBot({ mode: "LIVE", allow: [] })).withRule(
|
|
503
|
+
tokenBucket({
|
|
504
|
+
mode: "LIVE",
|
|
505
|
+
refillRate: 2_000,
|
|
506
|
+
interval: "1h",
|
|
507
|
+
capacity: 5_000,
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
export async function handler(req: Request) {
|
|
512
|
+
const decision = await routeAj.protect(req, { requested: 500 });
|
|
513
|
+
// ...
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**Other recommendations:**
|
|
518
|
+
|
|
519
|
+
- **Call `protect()` once per request** in the handler where you need it.
|
|
520
|
+
- **Start rules in `DRY_RUN` mode** to observe behavior before switching to
|
|
521
|
+
`LIVE`. This lets you tune thresholds without affecting real traffic.
|
|
522
|
+
- **Configure proxies** if your app runs behind a load balancer or reverse proxy
|
|
523
|
+
so Arcjet resolves the real client IP:
|
|
524
|
+
```ts
|
|
525
|
+
arcjet({ key: env.ARCJET_KEY!, rules: [], proxies: ["100.100.100.100"] });
|
|
526
|
+
```
|
|
527
|
+
- **Handle errors explicitly.** `protect()` never throws — on error it returns
|
|
528
|
+
an `ERROR` result. Fail open by logging and allowing the request:
|
|
529
|
+
```ts
|
|
530
|
+
if (decision.isErrored()) {
|
|
531
|
+
console.error("Arcjet error", decision.reason.message);
|
|
532
|
+
// allow the request to proceed
|
|
533
|
+
}
|
|
534
|
+
```
|
|
108
535
|
|
|
109
536
|
## Test
|
|
110
537
|
|
|
@@ -119,11 +546,12 @@ bun test
|
|
|
119
546
|
|
|
120
547
|
[Apache License, Version 2.0][apache-license] © [Arcjet Labs, Inc.][arcjet]
|
|
121
548
|
|
|
122
|
-
[arcjet-get-started]: https://docs.arcjet.com/get-started
|
|
123
|
-
[arcjet-reference-bun]: https://docs.arcjet.com/reference/bun
|
|
124
549
|
[arcjet]: https://arcjet.com
|
|
550
|
+
[arcjet-reference-bun]: https://docs.arcjet.com/reference/bun
|
|
125
551
|
[bun-sh]: https://bun.sh/
|
|
126
|
-
[example-next-source]: https://github.com/arcjet/example-nextjs
|
|
127
|
-
[example-next-url]: https://example.arcjet.com
|
|
128
|
-
[quick-start]: https://docs.arcjet.com/get-started/bun
|
|
129
552
|
[apache-license]: http://www.apache.org/licenses/LICENSE-2.0
|
|
553
|
+
[bot-categories-docs]: https://docs.arcjet.com/bot-protection/identifying-bots
|
|
554
|
+
[bot-list]: https://arcjet.com/bot-list
|
|
555
|
+
[signup-protection-docs]: https://docs.arcjet.com/signup-protection
|
|
556
|
+
[filters-docs]: https://docs.arcjet.com/filters
|
|
557
|
+
[best-practices]: https://docs.arcjet.com/best-practices
|
package/index.js
CHANGED
|
@@ -26,7 +26,7 @@ function createRemoteClient(options) {
|
|
|
26
26
|
// Transport is the HTTP client that the client uses to make requests.
|
|
27
27
|
const transport = createTransport(url);
|
|
28
28
|
const sdkStack = "BUN";
|
|
29
|
-
const sdkVersion = "1.
|
|
29
|
+
const sdkVersion = "1.4.0";
|
|
30
30
|
return createClient({
|
|
31
31
|
transport,
|
|
32
32
|
baseUrl: url,
|
package/package.json
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcjet/bun",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Arcjet SDK for Bun",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Arcjet runtime security SDK for Bun — bot protection, rate limiting, prompt injection detection, PII blocking, and WAF",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"ai",
|
|
6
7
|
"analyze",
|
|
7
8
|
"arcjet",
|
|
8
9
|
"attack",
|
|
9
|
-
"
|
|
10
|
+
"bot-detection",
|
|
10
11
|
"bun",
|
|
12
|
+
"bun.sh",
|
|
11
13
|
"limit",
|
|
14
|
+
"llm",
|
|
15
|
+
"pii",
|
|
16
|
+
"prompt-injection",
|
|
12
17
|
"protect",
|
|
18
|
+
"rate-limiting",
|
|
13
19
|
"secure",
|
|
14
20
|
"security",
|
|
15
|
-
"verify"
|
|
21
|
+
"verify",
|
|
22
|
+
"waf"
|
|
16
23
|
],
|
|
17
24
|
"license": "Apache-2.0",
|
|
18
25
|
"homepage": "https://arcjet.com",
|
|
@@ -44,23 +51,23 @@
|
|
|
44
51
|
"test": "npm run build && npm run lint"
|
|
45
52
|
},
|
|
46
53
|
"dependencies": {
|
|
47
|
-
"@arcjet/body": "1.
|
|
48
|
-
"@arcjet/env": "1.
|
|
49
|
-
"@arcjet/headers": "1.
|
|
50
|
-
"@arcjet/ip": "1.
|
|
51
|
-
"@arcjet/logger": "1.
|
|
52
|
-
"@arcjet/protocol": "1.
|
|
53
|
-
"@arcjet/transport": "1.
|
|
54
|
-
"arcjet": "1.
|
|
54
|
+
"@arcjet/body": "1.4.0",
|
|
55
|
+
"@arcjet/env": "1.4.0",
|
|
56
|
+
"@arcjet/headers": "1.4.0",
|
|
57
|
+
"@arcjet/ip": "1.4.0",
|
|
58
|
+
"@arcjet/logger": "1.4.0",
|
|
59
|
+
"@arcjet/protocol": "1.4.0",
|
|
60
|
+
"@arcjet/transport": "1.4.0",
|
|
61
|
+
"arcjet": "1.4.0"
|
|
55
62
|
},
|
|
56
63
|
"devDependencies": {
|
|
57
|
-
"@arcjet/eslint-config": "1.
|
|
58
|
-
"@arcjet/rollup-config": "1.
|
|
59
|
-
"@rollup/wasm-node": "4.
|
|
60
|
-
"bun-types": "1.3.
|
|
61
|
-
"eslint": "9.39.
|
|
64
|
+
"@arcjet/eslint-config": "1.4.0",
|
|
65
|
+
"@arcjet/rollup-config": "1.4.0",
|
|
66
|
+
"@rollup/wasm-node": "4.59.0",
|
|
67
|
+
"bun-types": "1.3.10",
|
|
68
|
+
"eslint": "9.39.3",
|
|
62
69
|
"typescript": "5.9.3",
|
|
63
|
-
"undici-types": "7.
|
|
70
|
+
"undici-types": "7.22.0"
|
|
64
71
|
},
|
|
65
72
|
"publishConfig": {
|
|
66
73
|
"access": "public",
|