From 6c0309c7ceb911547a33a550029ddd485e9ef1f6 Mon Sep 17 00:00:00 2001 From: Blaine Bublitz Date: Mon, 5 Feb 2024 16:19:28 -0700 Subject: [PATCH] feat!: Build extra field from unknown request properties --- arcjet-next/index.ts | 4 +- arcjet/index.ts | 80 ++++++++-- arcjet/test/index.node.test.ts | 261 ++++++++++++++++----------------- protocol/index.ts | 1 - 4 files changed, 191 insertions(+), 155 deletions(-) diff --git a/arcjet-next/index.ts b/arcjet-next/index.ts index d86514755..db378c6bb 100644 --- a/arcjet-next/index.ts +++ b/arcjet-next/index.ts @@ -191,7 +191,7 @@ export default function arcjetNext( path = request.url ?? ""; } - let extra: { [key: string]: string } = {}; + const extra: { [key: string]: string } = {}; // If we're running on Vercel, we can add some extra information if (process.env["VERCEL"]) { @@ -217,7 +217,7 @@ export default function arcjetNext( host, path, headers, - extra, + ...extra, // TODO(#220): The generic manipulations get really mad here, so we just cast it } as ArcjetRequest>); diff --git a/arcjet/index.ts b/arcjet/index.ts index 5ef873eea..ef9c5729a 100644 --- a/arcjet/index.ts +++ b/arcjet/index.ts @@ -197,6 +197,49 @@ export function defaultBaseUrl() { } } +const knownFields = [ + "ip", + "method", + "protocol", + "host", + "path", + "headers", + "body", + "email", + "cookies", + "query", +]; + +function isUnknownRequestProperty(key: string) { + return !knownFields.includes(key); +} + +function toString(value: unknown) { + if (typeof value === "string") { + return value; + } + + if (typeof value === "number") { + return `${value}`; + } + + if (typeof value === "boolean") { + return value ? "true" : "false"; + } + + return ""; +} + +function extraProps(details: ArcjetRequestDetails): Record { + const extra: Map = new Map(); + for (const [key, value] of Object.entries(details)) { + if (isUnknownRequestProperty(key)) { + extra.set(key, toString(value)); + } + } + return Object.fromEntries(extra.entries()); +} + export function createRemoteClient( options?: RemoteClientOptions, ): RemoteClient { @@ -240,7 +283,7 @@ export function createRemoteClient( headers: Object.fromEntries(details.headers.entries()), // TODO(#208): Re-add body // body: details.body, - extra: details.extra, + extra: extraProps(details), email: typeof details.email === "string" ? details.email : undefined, }, rules: rules.map(ArcjetRuleToProtocol), @@ -289,7 +332,7 @@ export function createRemoteClient( headers: Object.fromEntries(details.headers.entries()), // TODO(#208): Re-add body // body: details.body, - extra: details.extra, + extra: extraProps(details), email: typeof details.email === "string" ? details.email : undefined, }, decision: ArcjetDecisionToProtocol(decision), @@ -482,6 +525,12 @@ const Priority = { type PlainObject = { [key: string]: unknown }; +// Primitives and Products external names for Rules even though they are defined +// the same. +// See ExtraProps below for further explanation on why we define them like this. +export type Primitive = ArcjetRule[]; +export type Product = ArcjetRule[]; + type PropsForRule = R extends ArcjetRule ? Props : {}; // We theoretically support an arbitrary amount of rule flattening, // but one level seems to be easiest; however, this puts a constraint of @@ -495,15 +544,22 @@ export type ExtraProps = Rules extends [] ? UnionToIntersection> : never; +/** + * @property {string} ip - The IP address of the client. + * @property {string} method - The HTTP method of the request. + * @property {string} protocol - The protocol of the request. + * @property {string} host - The host of the request. + * @property {string} path - The path of the request. + * @property {Headers} headers - The headers of the request. + * @property {string} cookies - The string representing semicolon-separated Cookies for a request. + * @property {string} query - The `?`-prefixed string representing the Query for a request. Commonly referred to as a "querystring". + * @property {string} email - An email address related to the request. + * @property ...extra - Extra data that might be useful for Arcjet. For example, requested tokens are specified as the `requested` property. + */ export type ArcjetRequest = Simplify< Partial >; -// Primitives and Products are the external names for Rules even though they are defined the same -// See ArcjetRequest above for the explanation on why we define them like this. -export type Primitive = ArcjetRule[]; -export type Product = ArcjetRule[]; - function isLocalRule( rule: ArcjetRule, ): rule is ArcjetLocalRule { @@ -770,15 +826,7 @@ export interface Arcjet { * Make a decision about how to handle a request. This will analyze the * request locally where possible and call the Arcjet decision API. * - * @param {ArcjetRequest} request - The details about the request that Arcjet needs to make a decision. - * @param {string} request.ip - The IP address of the client. - * @param {string} request.method - The HTTP method of the request. - * @param {string} request.protocol - The protocol of the request. - * @param {string} request.host - The host of the request. - * @param {string} request.path - The path of the request. - * @param {Headers} request.headers - The headers of the request. - * @param request.extra - Extra data to send to the Arcjet API. - * + * @param {ArcjetRequest} request - Details about the {@link ArcjetRequest} that Arcjet needs to make a decision. * @returns An {@link ArcjetDecision} indicating Arcjet's decision about the request. */ protect(request: ArcjetRequest): Promise; diff --git a/arcjet/test/index.node.test.ts b/arcjet/test/index.node.test.ts index a7dbd7304..cbdc49b20 100644 --- a/arcjet/test/index.node.test.ts +++ b/arcjet/test/index.node.test.ts @@ -212,9 +212,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const router = { @@ -238,7 +236,15 @@ describe("createRemoteClient", () => { expect(router.decide).toHaveBeenCalledTimes(1); expect(router.decide).toHaveBeenCalledWith( new DecideRequest({ - details: { ...details, headers: { "user-agent": "curl/8.1.2" } }, + details: { + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, + headers: { "user-agent": "curl/8.1.2" }, + }, fingerprint, rules: [], sdkStack: SDKStack.SDK_STACK_NEXTJS, @@ -264,9 +270,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const router = { @@ -291,7 +295,15 @@ describe("createRemoteClient", () => { expect(router.decide).toHaveBeenCalledTimes(1); expect(router.decide).toHaveBeenCalledWith( new DecideRequest({ - details: { ...details, headers: { "user-agent": "curl/8.1.2" } }, + details: { + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, + headers: { "user-agent": "curl/8.1.2" }, + }, fingerprint, rules: [], sdkStack: SDKStack.SDK_STACK_UNSPECIFIED, @@ -317,9 +329,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const router = { @@ -342,7 +352,15 @@ describe("createRemoteClient", () => { expect(router.decide).toHaveBeenCalledTimes(1); expect(router.decide).toHaveBeenCalledWith( new DecideRequest({ - details: { ...details, headers: { "user-agent": "curl/8.1.2" } }, + details: { + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, + headers: { "user-agent": "curl/8.1.2" }, + }, fingerprint, rules: [], sdkStack: SDKStack.SDK_STACK_NODEJS, @@ -368,9 +386,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", email: "abc@example.com", }; @@ -394,7 +410,16 @@ describe("createRemoteClient", () => { expect(router.decide).toHaveBeenCalledTimes(1); expect(router.decide).toHaveBeenCalledWith( new DecideRequest({ - details: { ...details, headers: { "user-agent": "curl/8.1.2" } }, + details: { + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, + headers: { "user-agent": "curl/8.1.2" }, + email: details.email, + }, fingerprint, rules: [], sdkStack: SDKStack.SDK_STACK_NODEJS, @@ -420,9 +445,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", email: "abc@example.com", }; @@ -451,7 +474,16 @@ describe("createRemoteClient", () => { expect(router.decide).toHaveBeenCalledTimes(1); expect(router.decide).toHaveBeenCalledWith( new DecideRequest({ - details: { ...details, headers: { "user-agent": "curl/8.1.2" } }, + details: { + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, + headers: { "user-agent": "curl/8.1.2" }, + email: details.email, + }, fingerprint, rules: [new Rule()], sdkStack: SDKStack.SDK_STACK_NODEJS, @@ -477,9 +509,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const router = { @@ -519,9 +549,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const router = { @@ -560,9 +588,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const router = { @@ -601,9 +627,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const router = { @@ -645,9 +669,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const router = { @@ -695,9 +717,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const router = { @@ -738,11 +758,8 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", email: "test@example.com", - receivedAt, }; const [promise, resolve] = deferred(); @@ -775,8 +792,14 @@ describe("createRemoteClient", () => { sdkVersion: "__ARCJET_SDK_VERSION__", fingerprint, details: { - ...details, + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, headers: { "user-agent": "curl/8.1.2" }, + email: details.email, }, decision: { id: decision.id, @@ -807,10 +830,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, - receivedAt, + "extra-test": "extra-test-value", }; const [promise, resolve] = deferred(); @@ -843,7 +863,12 @@ describe("createRemoteClient", () => { sdkStack: SDKStack.SDK_STACK_NODEJS, sdkVersion: "__ARCJET_SDK_VERSION__", details: { - ...details, + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, headers: { "user-agent": "curl/8.1.2" }, }, decision: { @@ -875,10 +900,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, - receivedAt, + "extra-test": "extra-test-value", }; const [promise, resolve] = deferred(); @@ -911,7 +933,12 @@ describe("createRemoteClient", () => { sdkVersion: "__ARCJET_SDK_VERSION__", fingerprint, details: { - ...details, + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, headers: { "user-agent": "curl/8.1.2" }, }, decision: { @@ -950,10 +977,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, - receivedAt, + "extra-test": "extra-test-value", }; const [promise, resolve] = deferred(); @@ -986,7 +1010,12 @@ describe("createRemoteClient", () => { sdkVersion: "__ARCJET_SDK_VERSION__", fingerprint, details: { - ...details, + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, headers: { "user-agent": "curl/8.1.2" }, }, decision: { @@ -1018,9 +1047,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const [promise, resolve] = deferred(); @@ -1049,7 +1076,12 @@ describe("createRemoteClient", () => { sdkVersion: "__ARCJET_SDK_VERSION__", fingerprint, details: { - ...details, + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, headers: { "user-agent": "curl/8.1.2" }, }, decision: { @@ -1081,9 +1113,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", email: "abc@example.com", }; @@ -1130,8 +1160,14 @@ describe("createRemoteClient", () => { sdkVersion: "__ARCJET_SDK_VERSION__", fingerprint, details: { - ...details, + ip: details.ip, + method: details.method, + protocol: details.protocol, + host: details.host, + path: details.path, + extra: { "extra-test": details["extra-test"] }, headers: { "user-agent": "curl/8.1.2" }, + email: details.email, }, decision: { id: decision.id, @@ -1169,9 +1205,7 @@ describe("createRemoteClient", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const [promise, resolve] = deferred(); @@ -1583,9 +1617,7 @@ describe("Primitives > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const [rule] = detectBot(options); @@ -1635,9 +1667,7 @@ describe("Primitives > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const [rule] = detectBot(options); @@ -1687,9 +1717,7 @@ describe("Primitives > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const [rule] = detectBot(options); @@ -1726,9 +1754,7 @@ describe("Primitives > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const [rule] = detectBot({ @@ -1782,9 +1808,7 @@ describe("Primitives > detectBot", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const [rule] = detectBot(options); @@ -1833,9 +1857,7 @@ describe("Primitives > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const [rule] = detectBot(options); @@ -1873,9 +1895,7 @@ describe("Primitives > detectBot", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const [rule] = detectBot(options); @@ -2663,9 +2683,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const allowed = testRuleLocalAllowed(); const denied = testRuleLocalDenied(); @@ -2781,9 +2799,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const allowed = testRuleLocalAllowed(); const denied = testRuleLocalDenied(); @@ -2828,9 +2844,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const allowed = testRuleLocalAllowed(); @@ -2871,9 +2885,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const rule = testRuleLocalAllowed(); @@ -2917,9 +2929,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const rule = testRuleLocalDenied(); @@ -2961,9 +2971,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const denied = testRuleLocalDenied(); @@ -3002,9 +3010,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "Mozilla/5.0"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const aj = arcjet({ @@ -3044,9 +3050,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "Mozilla/5.0"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const aj = arcjet({ @@ -3119,9 +3123,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "Mozilla/5.0"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const aj = arcjet({ @@ -3157,9 +3159,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "Mozilla/5.0"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; let errorLogSpy; @@ -3213,9 +3213,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "Mozilla/5.0"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; let errorLogSpy; @@ -3269,9 +3267,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "Mozilla/5.0"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const aj = arcjet({ @@ -3320,9 +3316,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "Mozilla/5.0"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const rule = testRuleRemote(); @@ -3366,9 +3360,7 @@ describe("SDK", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "Mozilla/5.0"]]), - extra: { - "extra-test": "extra-test-value", - }, + "extra-test": "extra-test-value", }; const aj = arcjet({ @@ -3449,9 +3441,6 @@ describe("Arcjet: Env = Serverless Node runtime on Vercel", () => { const protocol = "http"; const host = "example.com"; const path = "/"; - const extra: { [key: string]: string } = { - "extra-test": "extra-test-value", - }; const headers = new Headers(); headers.append("User-Agent", "Mozilla/5.0"); @@ -3467,7 +3456,7 @@ describe("Arcjet: Env = Serverless Node runtime on Vercel", () => { host, path, headers, - extra, + "extra-test": "extra-test-value", }); // If this fails, check the console an error related to the args passed to diff --git a/protocol/index.ts b/protocol/index.ts index dcb12ad74..e0eb57c98 100644 --- a/protocol/index.ts +++ b/protocol/index.ts @@ -375,7 +375,6 @@ export interface ArcjetRequestDetails { path: string; // TODO(#215): Allow `Record` and `Record`? headers: Headers; - extra: Record; } export type ArcjetRule = {