@arcjet/bun 1.0.0-beta.8 → 1.0.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.
Files changed (4) hide show
  1. package/README.md +50 -62
  2. package/index.d.ts +93 -30
  3. package/index.js +59 -51
  4. package/package.json +33 -26
package/README.md CHANGED
@@ -22,6 +22,9 @@ against common attacks.
22
22
 
23
23
  This is the [Arcjet][arcjet] SDK for [Bun.sh][bun-sh].
24
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)
27
+
25
28
  ## Getting started
26
29
 
27
30
  Visit the [quick start guide][quick-start] to get started.
@@ -31,100 +34,85 @@ Visit the [quick start guide][quick-start] to get started.
31
34
  Try an Arcjet protected app live at [https://example.arcjet.com][example-url]
32
35
  ([source code][example-source]).
33
36
 
34
- ## Installation
37
+ ## What is this?
35
38
 
36
- ```shell
37
- bun add @arcjet/bun
38
- ```
39
+ This is our adapter to integrate Arcjet into Bun.
40
+ Arcjet helps you secure your Bun server.
41
+ This package exists so that we can provide the best possible experience to
42
+ Bun users.
39
43
 
40
- ## Rate limit + bot detection example
44
+ ## When should I use this?
41
45
 
42
- The [Arcjet rate limit][rate-limit-concepts-docs] example below applies a token
43
- bucket rate limit rule to a route where we identify the user based on their ID
44
- e.g. if they are logged in. The bucket is configured with a maximum capacity of
45
- 10 tokens and refills by 5 tokens every 10 seconds. Each request consumes 5
46
- tokens.
46
+ You can use this if you are using Bun.
47
+ See our [_Get started_ guide][arcjet-get-started] for other supported
48
+ runtimes.
47
49
 
48
- ```ts
49
- import arcjet, { tokenBucket, detectBot } from "@arcjet/bun";
50
+ ## Install
50
51
 
51
- const aj = arcjet({
52
- key: Bun.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
53
- characteristics: ["userId"], // track requests by a custom user ID
54
- rules: [
55
- // Create a token bucket rate limit. Other algorithms are supported.
56
- tokenBucket({
57
- mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
58
- refillRate: 5, // refill 5 tokens per interval
59
- interval: 10, // refill every 10 seconds
60
- capacity: 10, // bucket maximum capacity of 10 tokens
61
- }),
62
- detectBot({
63
- mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
64
- // configured with a list of bots to allow from
65
- // https://arcjet.com/bot-list
66
- allow: [], // "allow none" will block all detected bots
67
- }),
68
- ],
69
- });
70
-
71
- export default {
72
- port: 8000,
73
- fetch: aj.handler(async function (req: Request) {
74
- const userId = "user123"; // Replace with your authenticated user ID
75
- const decision = await aj.protect(req, { userId, requested: 5 }); // Deduct 5 tokens from the bucket
76
- console.log("Arcjet decision", decision);
52
+ This package is ESM only.
53
+ Install with Bun:
77
54
 
78
- if (decision.isDenied()) {
79
- return Response.json({ error: "Forbidden" }, { status: 403 });
80
- } else {
81
- return Response.json({ message: "Hello world" });
82
- }
83
- }),
84
- };
55
+ ```sh
56
+ bun add @arcjet/bun
85
57
  ```
86
58
 
87
- ## Shield example
88
-
89
- [Arcjet Shield][shield-concepts-docs] protects your application against common
90
- attacks, including the OWASP Top 10. You can run Shield on every request with
91
- negligible performance impact.
59
+ ## Use
92
60
 
93
61
  ```ts
94
62
  import arcjet, { shield } from "@arcjet/bun";
95
63
 
64
+ // Get your Arcjet key at <https://app.arcjet.com>.
65
+ // Set it as an environment variable instead of hard coding it.
66
+ const arcjetKey = Bun.env.ARCJET_KEY;
67
+
68
+ if (!arcjetKey) {
69
+ throw new Error("Cannot find `ARCJET_KEY` environment variable");
70
+ }
71
+
96
72
  const aj = arcjet({
97
- key: Bun.env.ARCJET_KEY, // Get your site key from https://app.arcjet.com
73
+ key: arcjetKey,
98
74
  rules: [
99
- shield({
100
- mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
101
- }),
75
+ // Shield protects your app from common attacks.
76
+ // Use `DRY_RUN` instead of `LIVE` to only log.
77
+ shield({ mode: "LIVE" }),
102
78
  ],
103
79
  });
104
80
 
105
81
  export default {
106
82
  port: 8000,
107
- fetch: aj.handler(async function (req: Request) {
108
- const decision = await aj.protect(req);
83
+ fetch: aj.handler(async function (request: Request) {
84
+ const decision = await aj.protect(request);
109
85
 
110
86
  if (decision.isDenied()) {
111
- return Response.json({ error: "Forbidden" }, { status: 403 });
112
- } else {
113
- return Response.json({ message: "Hello world" });
87
+ return Response.json({ message: "Forbidden" }, { status: 403 });
114
88
  }
89
+
90
+ return Response.json({ message: "Hello world" });
115
91
  }),
116
92
  };
117
93
  ```
118
94
 
95
+ For more on how to configure Arcjet with Bun and how to protect Bun,
96
+ see the [Arcjet Bun SDK reference][arcjet-reference-bun] on our website.
97
+
98
+ ## Test
99
+
100
+ This monorepo is tested with Node.js but this package targets Bun.
101
+ Run the tests from this package folder with:
102
+
103
+ ```sh
104
+ bun test
105
+ ```
106
+
119
107
  ## License
120
108
 
121
- Licensed under the [Apache License, Version 2.0][apache-license].
109
+ [Apache License, Version 2.0][apache-license] © [Arcjet Labs, Inc.][arcjet]
122
110
 
111
+ [arcjet-get-started]: https://docs.arcjet.com/get-started
112
+ [arcjet-reference-bun]: https://docs.arcjet.com/reference/bun
123
113
  [arcjet]: https://arcjet.com
124
114
  [bun-sh]: https://bun.sh/
125
115
  [example-url]: https://example.arcjet.com
126
116
  [quick-start]: https://docs.arcjet.com/get-started/bun
127
117
  [example-source]: https://github.com/arcjet/arcjet-js-example
128
- [rate-limit-concepts-docs]: https://docs.arcjet.com/rate-limiting/concepts
129
- [shield-concepts-docs]: https://docs.arcjet.com/shield/concepts
130
118
  [apache-license]: http://www.apache.org/licenses/LICENSE-2.0
package/index.d.ts CHANGED
@@ -5,64 +5,127 @@ type Simplify<T> = {
5
5
  [KeyType in keyof T]: T[KeyType];
6
6
  } & {};
7
7
  declare const emptyObjectSymbol: unique symbol;
8
- type WithoutCustomProps = {
9
- [emptyObjectSymbol]?: never;
10
- };
11
8
  type PlainObject = {
12
9
  [key: string]: unknown;
13
10
  };
11
+ /**
12
+ * Dynamically generate whether zero or one `properties` object must or can be passed.
13
+ */
14
+ type MaybeProperties<T> = {
15
+ [P in keyof T]?: T[P];
16
+ } extends T ? T extends {
17
+ [emptyObjectSymbol]?: never;
18
+ } ? [
19
+ ] : [
20
+ properties?: T
21
+ ] : [
22
+ properties: T
23
+ ];
24
+ /**
25
+ * Configuration for {@linkcode createRemoteClient}.
26
+ */
14
27
  export type RemoteClientOptions = {
28
+ /**
29
+ * Base URI for HTTP requests to Decide API (optional).
30
+ *
31
+ * Defaults to the environment variable `ARCJET_BASE_URL` (if that value
32
+ * is known and allowed) and the standard production API otherwise.
33
+ */
15
34
  baseUrl?: string;
35
+ /**
36
+ * Timeout in milliseconds for the Decide API (optional).
37
+ *
38
+ * Defaults to `500` in production and `1000` in development.
39
+ */
16
40
  timeout?: number;
17
41
  };
42
+ /**
43
+ * Create a remote client.
44
+ *
45
+ * @param options
46
+ * Configuration (optional).
47
+ * @returns
48
+ * Client.
49
+ */
18
50
  export declare function createRemoteClient(options?: RemoteClientOptions): import("@arcjet/protocol/client.js").Client;
19
51
  /**
20
- * The options used to configure an {@link ArcjetBun} client.
52
+ * Configuration for the Bun integration of Arcjet.
53
+ *
54
+ * @template Rules
55
+ * List of rules.
56
+ * @template Characteristics
57
+ * Characteristics to track a user by.
21
58
  */
22
59
  export type ArcjetOptions<Rules extends [...Array<Primitive | Product>], Characteristics extends readonly string[]> = Simplify<CoreOptions<Rules, Characteristics> & {
23
60
  /**
24
- * One or more IP Address of trusted proxies in front of the application.
25
- * These addresses will be excluded when Arcjet detects a public IP address.
61
+ * IP addresses and CIDR ranges of trusted load balancers and proxies
62
+ * (optional, example: `["100.100.100.100", "100.100.100.0/24"]`).
26
63
  */
27
64
  proxies?: Array<string>;
28
65
  }>;
29
66
  /**
30
- * The ArcjetBun client provides a public `protect()` method to
31
- * make a decision about how a Bun.sh request should be handled.
67
+ * Instance of the Bun integration of Arcjet.
68
+ *
69
+ * Primarily has a `protect()` method to make a decision about how a Bun request
70
+ * should be handled.
71
+ *
72
+ * @template Props
73
+ * Configuration.
32
74
  */
33
75
  export interface ArcjetBun<Props extends PlainObject> {
34
76
  /**
35
- * Runs a request through the configured protections. The request is
36
- * analyzed and then a decision made on whether to allow, deny, or challenge
37
- * the request.
77
+ * Make a decision about how to handle a request.
38
78
  *
39
- * @param request - A `Request` provided to the fetch handler.
40
- * @param props - Additonal properties required for running rules against a request.
41
- * @returns An {@link ArcjetDecision} indicating Arcjet's decision about the request.
79
+ * This will analyze the request locally where possible and otherwise call
80
+ * the Arcjet decision API.
81
+ *
82
+ * @param ctx
83
+ * Additional context for this function call.
84
+ * @param request
85
+ * Details about the {@linkcode Request} that Arcjet needs to make a
86
+ * decision.
87
+ * @param props
88
+ * Additional properties required for running rules against a request.
89
+ * @returns
90
+ * Promise that resolves to an {@linkcode ArcjetDecision} indicating
91
+ * Arcjet’s decision about the request.
42
92
  */
43
- protect(request: Request, ...props: Props extends WithoutCustomProps ? [] : [Props]): Promise<ArcjetDecision>;
93
+ protect(request: Request, ...properties: MaybeProperties<Props>): Promise<ArcjetDecision>;
44
94
  /**
45
- * Augments the client with another rule. Useful for varying rules based on
46
- * criteria in your handler—e.g. different rate limit for logged in users.
95
+ * Augment the client with another rule.
96
+ *
97
+ * Useful for varying rules based on criteria in your handler such as
98
+ * different rate limit for logged in users.
47
99
  *
48
- * @param rule The rule to add to this execution.
49
- * @returns An augmented {@link ArcjetBun} client.
100
+ * @template Rule
101
+ * Type of rule.
102
+ * @param rule
103
+ * Rule to add to Arcjet.
104
+ * @returns
105
+ * Arcjet instance augmented with the given rule.
50
106
  */
51
- withRule<Rule extends Primitive | Product>(rule: Rule): ArcjetBun<Simplify<Props & ExtraProps<Rule>>>;
107
+ withRule<ChildProperties extends PlainObject>(rule: Primitive<ChildProperties> | Product<ChildProperties>): ArcjetBun<Props & ChildProperties>;
52
108
  /**
53
- * Wraps the Bun.sh `fetch` handler to provide additional Request details
54
- * when calling the SDK's `protect()` function.
109
+ * Wrap the Bun `fetch` handler to provide additional details when calling
110
+ * the `protect()` method.
55
111
  *
56
- * @param fetch: The user provided `fetch` handler
112
+ * @param fetch
113
+ * Original `fetch` from Bun.
114
+ * @returns
115
+ * Wrapped `fetch` handler.
57
116
  */
58
- handler(fetch: (this: Server, request: Request, server: Server) => Response | Promise<Response>): (this: Server, request: Request, server: Server) => Response | Promise<Response>;
117
+ handler(fetch: (this: Server<any>, request: Request, server: Server<any>) => Response | Promise<Response>): (this: Server<any>, request: Request, server: Server<any>) => Response | Promise<Response>;
59
118
  }
60
119
  /**
61
- * Create a new {@link ArcjetBun} client. Always build your initial client
62
- * outside of a request handler so it persists across requests. If you need to
63
- * augment a client inside a handler, call the `withRule()` function on the base
64
- * client.
120
+ * Create a new Bun integration of Arcjet.
65
121
  *
66
- * @param options - Arcjet configuration options to apply to all requests.
122
+ * @template Rules
123
+ * List of rules.
124
+ * @template Characteristics
125
+ * Characteristics to track a user by.
126
+ * @param options
127
+ * Configuration.
128
+ * @returns
129
+ * Bun integration of Arcjet.
67
130
  */
68
- export default function arcjet<const Rules extends (Primitive | Product)[], const Characteristics extends readonly string[]>(options: ArcjetOptions<Rules, Characteristics>): ArcjetBun<Simplify<ExtraProps<Rules> & CharacteristicProps<Characteristics>>>;
131
+ export default function arcjet<const Rules extends (Primitive | Product)[], const Characteristics extends readonly string[]>(options: ArcjetOptions<Rules, Characteristics>): ArcjetBun<ExtraProps<Rules> & CharacteristicProps<Characteristics>>;
package/index.js CHANGED
@@ -1,39 +1,32 @@
1
+ import * as core from 'arcjet';
1
2
  import core__default from 'arcjet';
2
3
  export * from 'arcjet';
3
- import findIP, { parseProxy } from '@arcjet/ip';
4
- import ArcjetHeaders from '@arcjet/headers';
4
+ import findIp, { parseProxy } from '@arcjet/ip';
5
+ import { ArcjetHeaders } from '@arcjet/headers';
5
6
  import { env } from 'bun';
7
+ import { readBodyWeb } from '@arcjet/body';
6
8
  import { logLevel, isDevelopment, baseUrl, platform } from '@arcjet/env';
7
9
  import { Logger } from '@arcjet/logger';
8
10
  import { createClient } from '@arcjet/protocol/client.js';
9
11
  import { createTransport } from '@arcjet/transport';
10
12
 
11
- /// <reference types="bun-types/bun.d.ts" />
12
- // TODO: Deduplicate with other packages
13
- function errorMessage(err) {
14
- if (err) {
15
- if (typeof err === "string") {
16
- return err;
17
- }
18
- if (typeof err === "object" &&
19
- "message" in err &&
20
- typeof err.message === "string") {
21
- return err.message;
22
- }
23
- }
24
- return "Unknown problem";
25
- }
13
+ /// <reference types="bun-types" />
14
+ let warnedForAutomaticBodyRead = false;
15
+ /**
16
+ * Create a remote client.
17
+ *
18
+ * @param options
19
+ * Configuration (optional).
20
+ * @returns
21
+ * Client.
22
+ */
26
23
  function createRemoteClient(options) {
27
- // The base URL for the Arcjet API. Will default to the standard production
28
- // API unless environment variable `ARCJET_BASE_URL` is set.
29
24
  const url = options?.baseUrl ?? baseUrl(env);
30
- // The timeout for the Arcjet API in milliseconds. This is set to a low value
31
- // in production so calls fail open.
32
25
  const timeout = options?.timeout ?? (isDevelopment(env) ? 1000 : 500);
33
26
  // Transport is the HTTP client that the client uses to make requests.
34
27
  const transport = createTransport(url);
35
28
  const sdkStack = "BUN";
36
- const sdkVersion = "1.0.0-beta.8";
29
+ const sdkVersion = "1.0.0";
37
30
  return createClient({
38
31
  transport,
39
32
  baseUrl: url,
@@ -43,12 +36,16 @@ function createRemoteClient(options) {
43
36
  });
44
37
  }
45
38
  /**
46
- * Create a new {@link ArcjetBun} client. Always build your initial client
47
- * outside of a request handler so it persists across requests. If you need to
48
- * augment a client inside a handler, call the `withRule()` function on the base
49
- * client.
39
+ * Create a new Bun integration of Arcjet.
50
40
  *
51
- * @param options - Arcjet configuration options to apply to all requests.
41
+ * @template Rules
42
+ * List of rules.
43
+ * @template Characteristics
44
+ * Characteristics to track a user by.
45
+ * @param options
46
+ * Configuration.
47
+ * @returns
48
+ * Bun integration of Arcjet.
52
49
  */
53
50
  function arcjet(options) {
54
51
  const client = options.client ?? createRemoteClient();
@@ -64,21 +61,25 @@ function arcjet(options) {
64
61
  const proxies = Array.isArray(options.proxies)
65
62
  ? options.proxies.map(parseProxy)
66
63
  : undefined;
67
- if (isDevelopment(process.env)) {
64
+ if (isDevelopment(env)) {
68
65
  log.warn("Arcjet will use 127.0.0.1 when missing public IP address in development mode");
69
66
  }
70
67
  function toArcjetRequest(request, props) {
71
- const cookies = request.headers.get("cookie") ?? undefined;
68
+ const cookies = request.headers.get("cookie") ?? "";
72
69
  // We construct an ArcjetHeaders to normalize over Headers
73
70
  const headers = new ArcjetHeaders(request.headers);
74
71
  const url = new URL(request.url);
75
- let ip = findIP({
76
- // This attempts to lookup the IP in the `ipCache`. This is primarily a
77
- // workaround to the API design in Bun that requires access to the
78
- // `Server` to lookup an IP.
79
- ip: ipCache.get(request),
80
- headers,
81
- }, { platform: platform(env), proxies });
72
+ const xArcjetIp = isDevelopment(env)
73
+ ? headers.get("x-arcjet-ip")
74
+ : undefined;
75
+ let ip = xArcjetIp ||
76
+ findIp({
77
+ // This attempts to lookup the IP in the `ipCache`. This is primarily a
78
+ // workaround to the API design in Bun that requires access to the
79
+ // `Server` to lookup an IP.
80
+ ip: ipCache.get(request),
81
+ headers,
82
+ }, { platform: platform(env), proxies });
82
83
  if (ip === "") {
83
84
  // If the `ip` is empty but we're in development mode, we default the IP
84
85
  // so the request doesn't fail.
@@ -102,27 +103,33 @@ function arcjet(options) {
102
103
  };
103
104
  }
104
105
  function withClient(aj) {
105
- return Object.freeze({
106
+ const client = {
106
107
  withRule(rule) {
107
108
  const client = aj.withRule(rule);
108
109
  return withClient(client);
109
110
  },
110
- async protect(request, ...[props]) {
111
- // TODO(#220): The generic manipulations get really mad here, so we cast
112
- // Further investigation makes it seem like it has something to do with
113
- // the definition of `props` in the signature but it's hard to track down
114
- const req = toArcjetRequest(request, props ?? {});
111
+ async protect(request, props) {
112
+ // Cast of `{}` because here we switch from `undefined` to `Properties`.
113
+ const req = toArcjetRequest(request, props || {});
115
114
  const getBody = async () => {
116
- try {
117
- const clonedRequest = request.clone();
118
- // Awaited to throw if it rejects and we'll just return undefined
119
- const body = await clonedRequest.text();
120
- return body;
115
+ if (request.bodyUsed) {
116
+ throw new Error("Cannot read body: already read");
117
+ }
118
+ const clonedRequest = request.clone();
119
+ let expectedLength;
120
+ const expectedLengthString = request.headers.get("content-length");
121
+ if (typeof expectedLengthString === "string") {
122
+ expectedLength = parseInt(expectedLengthString, 10);
121
123
  }
122
- catch (e) {
123
- log.error("failed to get request body: %s", errorMessage(e));
124
- return;
124
+ // HEAD and GET requests do not have a body.
125
+ if (!clonedRequest.body) {
126
+ throw new Error("Cannot read body: body is missing");
125
127
  }
128
+ if (!warnedForAutomaticBodyRead) {
129
+ warnedForAutomaticBodyRead = true;
130
+ log.warn("Automatically reading the request body is deprecated; please pass an explicit `sensitiveInfoValue` field. See <https://docs.arcjet.com/upgrading/sdk-migration>.");
131
+ }
132
+ return readBodyWeb(clonedRequest.body, { expectedLength });
126
133
  };
127
134
  return aj.protect({ getBody }, req);
128
135
  },
@@ -135,7 +142,8 @@ function arcjet(options) {
135
142
  return fetch.call(server, request, server);
136
143
  };
137
144
  },
138
- });
145
+ };
146
+ return Object.freeze(client);
139
147
  }
140
148
  const aj = core__default({ ...options, client, log });
141
149
  return withClient(aj);
package/package.json CHANGED
@@ -1,7 +1,19 @@
1
1
  {
2
2
  "name": "@arcjet/bun",
3
- "version": "1.0.0-beta.8",
4
- "description": "Arcjet SDK for Bun.sh",
3
+ "version": "1.0.0",
4
+ "description": "Arcjet SDK for Bun",
5
+ "keywords": [
6
+ "analyze",
7
+ "arcjet",
8
+ "attack",
9
+ "bun.sh",
10
+ "bun",
11
+ "limit",
12
+ "protect",
13
+ "secure",
14
+ "security",
15
+ "verify"
16
+ ],
5
17
  "license": "Apache-2.0",
6
18
  "homepage": "https://arcjet.com",
7
19
  "repository": {
@@ -23,38 +35,33 @@
23
35
  "main": "./index.js",
24
36
  "types": "./index.d.ts",
25
37
  "files": [
26
- "LICENSE",
27
- "README.md",
28
- "*.js",
29
- "*.d.ts",
30
- "!*.config.js"
38
+ "index.d.ts",
39
+ "index.js"
31
40
  ],
32
41
  "scripts": {
33
- "prepublishOnly": "npm run build",
34
42
  "build": "rollup --config rollup.config.js",
35
43
  "lint": "eslint .",
36
- "pretest": "npm run build",
37
- "test": "node --test --experimental-test-coverage"
44
+ "prepublishOnly": "npm run build",
45
+ "test": "npm run build && npm run lint"
38
46
  },
39
47
  "dependencies": {
40
- "@arcjet/env": "1.0.0-beta.8",
41
- "@arcjet/headers": "1.0.0-beta.8",
42
- "@arcjet/ip": "1.0.0-beta.8",
43
- "@arcjet/logger": "1.0.0-beta.8",
44
- "@arcjet/protocol": "1.0.0-beta.8",
45
- "@arcjet/transport": "1.0.0-beta.8",
46
- "arcjet": "1.0.0-beta.8"
48
+ "@arcjet/body": "1.0.0",
49
+ "@arcjet/env": "1.0.0",
50
+ "@arcjet/headers": "1.0.0",
51
+ "@arcjet/ip": "1.0.0",
52
+ "@arcjet/logger": "1.0.0",
53
+ "@arcjet/protocol": "1.0.0",
54
+ "@arcjet/transport": "1.0.0",
55
+ "arcjet": "1.0.0"
47
56
  },
48
57
  "devDependencies": {
49
- "@arcjet/eslint-config": "1.0.0-beta.8",
50
- "@arcjet/rollup-config": "1.0.0-beta.8",
51
- "@arcjet/tsconfig": "1.0.0-beta.8",
52
- "@types/node": "20.12.12",
53
- "@rollup/wasm-node": "4.41.1",
54
- "bun-types": "1.2.14",
55
- "eslint": "9.27.0",
56
- "expect": "29.7.0",
57
- "typescript": "5.8.3"
58
+ "@arcjet/eslint-config": "1.0.0",
59
+ "@arcjet/rollup-config": "1.0.0",
60
+ "@rollup/wasm-node": "4.55.1",
61
+ "bun-types": "1.3.6",
62
+ "eslint": "9.39.2",
63
+ "typescript": "5.9.3",
64
+ "undici-types": "7.18.2"
58
65
  },
59
66
  "publishConfig": {
60
67
  "access": "public",