UNPKG

@arcjet/bun

Version:

Arcjet SDK for Bun

153 lines (150 loc) 5.37 kB
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 };