@arcjet/bun
Version:
Arcjet SDK for Bun
153 lines (150 loc) • 5.37 kB
JavaScript
import core__default from 'arcjet';
export * from 'arcjet';
import findIp, { parseProxy } from '@arcjet/ip';
import { ArcjetHeaders } from '@arcjet/headers';
import { env } from 'bun';
import { logLevel, isDevelopment, baseUrl, platform } from '@arcjet/env';
import { Logger } from '@arcjet/logger';
import { createClient } from '@arcjet/protocol/client.js';
import { createTransport } from '@arcjet/transport';
/// <reference types="bun-types" />
// TODO: Deduplicate with other packages
function errorMessage(err) {
if (err) {
if (typeof err === "string") {
return err;
}
if (typeof err === "object" &&
"message" in err &&
typeof err.message === "string") {
return err.message;
}
}
return "Unknown problem";
}
/**
* Create a remote client.
*
* @param options
* Configuration (optional).
* @returns
* Client.
*/
function createRemoteClient(options) {
const url = options?.baseUrl ?? baseUrl(env);
const timeout = options?.timeout ?? (isDevelopment(env) ? 1000 : 500);
// Transport is the HTTP client that the client uses to make requests.
const transport = createTransport(url);
const sdkStack = "BUN";
const sdkVersion = "1.0.0-beta.12";
return createClient({
transport,
baseUrl: url,
timeout,
sdkStack,
sdkVersion,
});
}
/**
* Create a new Bun integration of Arcjet.
*
* @template Rules
* List of rules.
* @template Characteristics
* Characteristics to track a user by.
* @param options
* Configuration.
* @returns
* Bun integration of Arcjet.
*/
function arcjet(options) {
const client = options.client ?? createRemoteClient();
// Assuming the `handler()` function was used around Bun's fetch handler this
// WeakMap should be populated with IP addresses inspected via
// `server.requestIP()`
const ipCache = new WeakMap();
const log = options.log
? options.log
: new Logger({
level: logLevel(env),
});
const proxies = Array.isArray(options.proxies)
? options.proxies.map(parseProxy)
: undefined;
if (isDevelopment(process.env)) {
log.warn("Arcjet will use 127.0.0.1 when missing public IP address in development mode");
}
function toArcjetRequest(request, props) {
const cookies = request.headers.get("cookie") ?? undefined;
// We construct an ArcjetHeaders to normalize over Headers
const headers = new ArcjetHeaders(request.headers);
const url = new URL(request.url);
let ip = findIp({
// This attempts to lookup the IP in the `ipCache`. This is primarily a
// workaround to the API design in Bun that requires access to the
// `Server` to lookup an IP.
ip: ipCache.get(request),
headers,
}, { platform: platform(env), proxies });
if (ip === "") {
// If the `ip` is empty but we're in development mode, we default the IP
// so the request doesn't fail.
if (isDevelopment(env)) {
ip = "127.0.0.1";
}
else {
log.warn(`Client IP address is missing. If this is a dev environment set the ARCJET_ENV env var to "development"`);
}
}
return {
...props,
ip,
method: request.method,
protocol: url.protocol,
host: url.host,
path: url.pathname,
headers,
cookies,
query: url.search,
};
}
function withClient(aj) {
return Object.freeze({
withRule(rule) {
const client = aj.withRule(rule);
return withClient(client);
},
async protect(request, ...[props]) {
// TODO(#220): The generic manipulations get really mad here, so we cast
// Further investigation makes it seem like it has something to do with
// the definition of `props` in the signature but it's hard to track down
const req = toArcjetRequest(request, props ?? {});
const getBody = async () => {
try {
const clonedRequest = request.clone();
// Awaited to throw if it rejects and we'll just return undefined
const body = await clonedRequest.text();
return body;
}
catch (e) {
log.error("failed to get request body: %s", errorMessage(e));
return;
}
};
return aj.protect({ getBody }, req);
},
handler(fetch) {
return async function (request, server) {
const socketAddress = server.requestIP(request);
if (socketAddress) {
ipCache.set(request, socketAddress.address);
}
return fetch.call(server, request, server);
};
},
});
}
const aj = core__default({ ...options, client, log });
return withClient(aj);
}
export { createRemoteClient, arcjet as default };