Skip to content

Commit

Permalink
feat!: Build extra field from unknown request properties (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
blaine-arcjet authored Feb 6, 2024
1 parent 6753117 commit 2576341
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 155 deletions.
4 changes: 2 additions & 2 deletions arcjet-next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function arcjetNext<const Rules extends (Primitive | Product)[]>(
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"]) {
Expand All @@ -217,7 +217,7 @@ export default function arcjetNext<const Rules extends (Primitive | Product)[]>(
host,
path,
headers,
extra,
...extra,
// TODO(#220): The generic manipulations get really mad here, so we just cast it
} as ArcjetRequest<ExtraProps<Rules>>);

Expand Down
80 changes: 64 additions & 16 deletions arcjet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<unsupported type>";
}

function extraProps(details: ArcjetRequestDetails): Record<string, string> {
const extra: Map<string, string> = 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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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<Props extends PlainObject = {}> = ArcjetRule<Props>[];
export type Product<Props extends PlainObject = {}> = ArcjetRule<Props>[];

type PropsForRule<R> = R extends ArcjetRule<infer Props> ? Props : {};
// We theoretically support an arbitrary amount of rule flattening,
// but one level seems to be easiest; however, this puts a constraint of
Expand All @@ -495,15 +544,22 @@ export type ExtraProps<Rules> = Rules extends []
? UnionToIntersection<PropsForRule<Rules[number]>>
: 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<Props extends PlainObject> = Simplify<
Partial<ArcjetRequestDetails & Props>
>;

// 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<Props extends PlainObject = {}> = ArcjetRule<Props>[];
export type Product<Props extends PlainObject = {}> = ArcjetRule<Props>[];

function isLocalRule<Props extends PlainObject>(
rule: ArcjetRule<Props>,
): rule is ArcjetLocalRule<Props> {
Expand Down Expand Up @@ -770,15 +826,7 @@ export interface Arcjet<Props extends PlainObject> {
* 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<Props>): Promise<ArcjetDecision>;
Expand Down
Loading

0 comments on commit 2576341

Please sign in to comment.