Skip to content

Commit

Permalink
feat!: Move all environment lookup into separate package (#897)
Browse files Browse the repository at this point in the history
This introduces a new `@arcjet/env` package that provides various functions which receive an "env" object (such as `Bun.env` or `process.env`) and look for various keys that we care about. These keys are part of the `Env` type exported by the package to have a centralized definition of the properties.

By separating this logic, we remove all Node.js or other runtime specific logic in our generic packages. The "env" object lookup is then done in each adapter.

Closes #51 
Closes #885 

TODO:
- [x] Tests for env package
- [x] Docs for env package
  • Loading branch information
blaine-arcjet authored Jun 10, 2024
1 parent a42fbd3 commit a5bb8ca
Show file tree
Hide file tree
Showing 30 changed files with 992 additions and 237 deletions.
1 change: 1 addition & 0 deletions .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"arcjet-sveltekit": "1.0.0-alpha.13",
"decorate": "1.0.0-alpha.13",
"duration": "1.0.0-alpha.13",
"env": "1.0.0-alpha.13",
"eslint-config": "1.0.0-alpha.13",
"headers": "1.0.0-alpha.13",
"ip": "1.0.0-alpha.13",
Expand Down
4 changes: 4 additions & 0 deletions .github/release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
"component": "@arcjet/duration",
"skip-github-release": true
},
"env": {
"component": "@arcjet/env",
"skip-github-release": true
},
"eslint-config": {
"component": "@arcjet/eslint-config",
"skip-github-release": true
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ find a specific one through the categories and descriptions below.
- [`@arcjet/runtime`](./runtime/README.md): Runtime detection.
- [`@arcjet/sprintf`](./sprintf/README.md): Platform-independent replacement for
`util.format`.
- [`@arcjet/env`](./env/README.md): Environment detection for Arcjet variables.

### Internal development

Expand Down
47 changes: 32 additions & 15 deletions arcjet-bun/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ import core, {
ExtraProps,
RemoteClient,
RemoteClientOptions,
defaultBaseUrl,
createRemoteClient,
Arcjet,
} from "arcjet";
import findIP from "@arcjet/ip";
import ArcjetHeaders from "@arcjet/headers";
import type { Server } from "bun";
import { env } from "bun";
import {
baseUrl,
isDevelopment,
isProduction,
logLevel,
platform,
} from "@arcjet/env";
import { Logger } from "@arcjet/logger";

// Re-export all named exports from the generic SDK
export * from "arcjet";
Expand Down Expand Up @@ -59,25 +66,35 @@ type PlainObject = {
};

export function createBunRemoteClient(
options?: RemoteClientOptions,
options?: Partial<RemoteClientOptions>,
): RemoteClient {
// The base URL for the Arcjet API. Will default to the standard production
// API unless environment variable `ARCJET_BASE_URL` is set.
const baseUrl = options?.baseUrl ?? defaultBaseUrl();
const url = options?.baseUrl ?? baseUrl(env);

// The timeout for the Arcjet API in milliseconds. This is set to a low value
// in production so calls fail open.
const timeout = options?.timeout ?? (isProduction(env) ? 500 : 1000);

// Transport is the HTTP client that the client uses to make requests.
const transport =
options?.transport ??
createConnectTransport({
baseUrl,
baseUrl: url,
httpVersion: "1.1",
});

// TODO(#223): Do we want to allow overrides to either of these? If not, we should probably define a separate type for `options`
// TODO(#223): Create separate options type to exclude these
const sdkStack = "BUN";
const sdkVersion = "__ARCJET_SDK_VERSION__";

return createRemoteClient({ ...options, transport, sdkStack, sdkVersion });
return createRemoteClient({
transport,
baseUrl: url,
timeout,
sdkStack,
sdkVersion,
});
}

/**
Expand Down Expand Up @@ -131,12 +148,6 @@ export interface ArcjetBun<Props extends PlainObject> {
) => Response | Promise<Response>;
}

function detectPlatform() {
if (typeof env["FLY_APP_NAME"] === "string" && env["FLY_APP_NAME"] !== "") {
return "fly-io" as const;
}
}

// This is provided with an `ipCache` where it attempts to lookup the IP. This
// is primarily a workaround to the API design in Bun that requires access to
// the `Server` to lookup an IP.
Expand All @@ -156,12 +167,12 @@ function toArcjetRequest<Props extends PlainObject>(
ip: ipCache.get(request),
},
headers,
{ platform: detectPlatform() },
{ platform: platform(env) },
);
if (ip === "") {
// If the `ip` is empty but we're in development mode, we default the IP
// so the request doesn't fail.
if (env.NODE_ENV === "development" || env.ARCJET_ENV === "development") {
if (isDevelopment(env)) {
// TODO: Log that the fingerprint is being overridden once the adapter
// constructs the logger
ip = "127.0.0.1";
Expand Down Expand Up @@ -246,7 +257,13 @@ export default function arcjet<const Rules extends (Primitive | Product)[]>(
): ArcjetBun<Simplify<ExtraProps<Rules>>> {
const client = options.client ?? createBunRemoteClient();

const aj = core({ ...options, client });
const log = options.log
? options.log
: new Logger({
level: logLevel(env),
});

const aj = core({ ...options, client, log });

return withClient(aj);
}
2 changes: 2 additions & 0 deletions arcjet-bun/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@
"test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests"
},
"dependencies": {
"@arcjet/env": "1.0.0-alpha.13",
"@arcjet/headers": "1.0.0-alpha.13",
"@arcjet/ip": "1.0.0-alpha.13",
"@arcjet/logger": "1.0.0-alpha.13",
"@connectrpc/connect-node": "1.4.0",
"arcjet": "1.0.0-alpha.13"
},
Expand Down
53 changes: 32 additions & 21 deletions arcjet-next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@ import arcjet, {
ExtraProps,
RemoteClient,
RemoteClientOptions,
defaultBaseUrl,
createRemoteClient,
Arcjet,
} from "arcjet";
import findIP from "@arcjet/ip";
import ArcjetHeaders from "@arcjet/headers";
import {
baseUrl,
isDevelopment,
isProduction,
logLevel,
platform,
} from "@arcjet/env";
import { Logger } from "@arcjet/logger";

// Re-export all named exports from the generic SDK
export * from "arcjet";
Expand Down Expand Up @@ -64,19 +71,23 @@ type PlainObject = {
};

export function createNextRemoteClient(
options?: RemoteClientOptions,
options?: Partial<RemoteClientOptions>,
): RemoteClient {
// The base URL for the Arcjet API. Will default to the standard production
// API unless environment variable `ARCJET_BASE_URL` is set.
const baseUrl = options?.baseUrl ?? defaultBaseUrl();
const url = options?.baseUrl ?? baseUrl(process.env);

// The timeout for the Arcjet API in milliseconds. This is set to a low value
// in production so calls fail open.
const timeout = options?.timeout ?? (isProduction(process.env) ? 500 : 1000);

// Transport is the HTTP client that the client uses to make requests.
// The Connect Node client doesn't work on edge runtimes: https://github.com/bufbuild/connect-es/pull/589
// so set the transport using connect-web. The interceptor is required for it work in the edge runtime.
const transport =
options?.transport ??
createConnectTransport({
baseUrl,
baseUrl: url,
interceptors: [
/**
* Ensures redirects are followed to properly support the Next.js/Vercel Edge
Expand All @@ -92,11 +103,17 @@ export function createNextRemoteClient(
fetch,
});

// TODO(#223): Do we want to allow overrides to either of these? If not, we should probably define a separate type for `options`
// TODO(#223): Create separate options type to exclude these
const sdkStack = "NEXTJS";
const sdkVersion = "__ARCJET_SDK_VERSION__";

return createRemoteClient({ ...options, transport, sdkStack, sdkVersion });
return createRemoteClient({
transport,
baseUrl: url,
timeout,
sdkStack,
sdkVersion,
});
}

// Interface of fields that the Arcjet Next.js SDK expects on `Request` objects.
Expand Down Expand Up @@ -193,30 +210,18 @@ export interface ArcjetNext<Props extends PlainObject> {
): ArcjetNext<Simplify<Props & ExtraProps<Rule>>>;
}

function detectPlatform() {
if (
typeof process.env["FLY_APP_NAME"] === "string" &&
process.env["FLY_APP_NAME"] !== ""
) {
return "fly-io" as const;
}
}

function toArcjetRequest<Props extends PlainObject>(
request: ArcjetNextRequest,
props: Props,
): ArcjetRequest<Props> {
// We construct an ArcjetHeaders to normalize over Headers
const headers = new ArcjetHeaders(request.headers);

let ip = findIP(request, headers, { platform: detectPlatform() });
let ip = findIP(request, headers, { platform: platform(process.env) });
if (ip === "") {
// If the `ip` is empty but we're in development mode, we default the IP
// so the request doesn't fail.
if (
process.env["NODE_ENV"] === "development" ||
process.env["ARCJET_ENV"] === "development"
) {
if (isDevelopment(process.env)) {
// TODO: Log that the fingerprint is being overridden once the adapter
// constructs the logger
ip = "127.0.0.1";
Expand Down Expand Up @@ -334,7 +339,13 @@ export default function arcjetNext<const Rules extends (Primitive | Product)[]>(
): ArcjetNext<Simplify<ExtraProps<Rules>>> {
const client = options.client ?? createNextRemoteClient();

const aj = arcjet({ ...options, client });
const log = options.log
? options.log
: new Logger({
level: logLevel(process.env),
});

const aj = arcjet({ ...options, client, log });

return withClient(aj);
}
Expand Down
2 changes: 2 additions & 0 deletions arcjet-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@
"test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests"
},
"dependencies": {
"@arcjet/env": "1.0.0-alpha.13",
"@arcjet/headers": "1.0.0-alpha.13",
"@arcjet/ip": "1.0.0-alpha.13",
"@arcjet/logger": "1.0.0-alpha.13",
"@connectrpc/connect-web": "1.4.0",
"arcjet": "1.0.0-alpha.13"
},
Expand Down
53 changes: 32 additions & 21 deletions arcjet-node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ import core, {
ExtraProps,
RemoteClient,
RemoteClientOptions,
defaultBaseUrl,
createRemoteClient,
Arcjet,
} from "arcjet";
import findIP from "@arcjet/ip";
import ArcjetHeaders from "@arcjet/headers";
import {
baseUrl,
isDevelopment,
isProduction,
logLevel,
platform,
} from "@arcjet/env";
import { Logger } from "@arcjet/logger";

// Re-export all named exports from the generic SDK
export * from "arcjet";
Expand Down Expand Up @@ -56,25 +63,35 @@ type PlainObject = {
};

export function createNodeRemoteClient(
options?: RemoteClientOptions,
options?: Partial<RemoteClientOptions>,
): RemoteClient {
// The base URL for the Arcjet API. Will default to the standard production
// API unless environment variable `ARCJET_BASE_URL` is set.
const baseUrl = options?.baseUrl ?? defaultBaseUrl();
const url = options?.baseUrl ?? baseUrl(process.env);

// The timeout for the Arcjet API in milliseconds. This is set to a low value
// in production so calls fail open.
const timeout = options?.timeout ?? (isProduction(process.env) ? 500 : 1000);

// Transport is the HTTP client that the client uses to make requests.
const transport =
options?.transport ??
createConnectTransport({
baseUrl,
baseUrl: url,
httpVersion: "2",
});

// TODO(#223): Do we want to allow overrides to either of these? If not, we should probably define a separate type for `options`
// TODO(#223): Create separate options type to exclude these
const sdkStack = "NODEJS";
const sdkVersion = "__ARCJET_SDK_VERSION__";

return createRemoteClient({ ...options, transport, sdkStack, sdkVersion });
return createRemoteClient({
transport,
baseUrl: url,
timeout,
sdkStack,
sdkVersion,
});
}

// Interface of fields that the Arcjet Node.js SDK expects on `IncomingMessage`
Expand Down Expand Up @@ -133,15 +150,6 @@ export interface ArcjetNode<Props extends PlainObject> {
): ArcjetNode<Simplify<Props & ExtraProps<Rule>>>;
}

function detectPlatform() {
if (
typeof process.env["FLY_APP_NAME"] === "string" &&
process.env["FLY_APP_NAME"] !== ""
) {
return "fly-io" as const;
}
}

function toArcjetRequest<Props extends PlainObject>(
request: ArcjetNodeRequest,
props: Props,
Expand All @@ -152,14 +160,11 @@ function toArcjetRequest<Props extends PlainObject>(
// We construct an ArcjetHeaders to normalize over Headers
const headers = new ArcjetHeaders(request.headers);

let ip = findIP(request, headers, { platform: detectPlatform() });
let ip = findIP(request, headers, { platform: platform(process.env) });
if (ip === "") {
// If the `ip` is empty but we're in development mode, we default the IP
// so the request doesn't fail.
if (
process.env["NODE_ENV"] === "development" ||
process.env["ARCJET_ENV"] === "development"
) {
if (isDevelopment(process.env)) {
// TODO: Log that the fingerprint is being overridden once the adapter
// constructs the logger
ip = "127.0.0.1";
Expand Down Expand Up @@ -246,7 +251,13 @@ export default function arcjet<const Rules extends (Primitive | Product)[]>(
): ArcjetNode<Simplify<ExtraProps<Rules>>> {
const client = options.client ?? createNodeRemoteClient();

const aj = core({ ...options, client });
const log = options.log
? options.log
: new Logger({
level: logLevel(process.env),
});

const aj = core({ ...options, client, log });

return withClient(aj);
}
2 changes: 2 additions & 0 deletions arcjet-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@
"test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests"
},
"dependencies": {
"@arcjet/env": "1.0.0-alpha.13",
"@arcjet/headers": "1.0.0-alpha.13",
"@arcjet/ip": "1.0.0-alpha.13",
"@arcjet/logger": "1.0.0-alpha.13",
"@connectrpc/connect-node": "1.4.0",
"arcjet": "1.0.0-alpha.13"
},
Expand Down
Loading

0 comments on commit a5bb8ca

Please sign in to comment.