Comparing version 1.0.0-alpha.13 to 1.0.0-alpha.14
@@ -1,2 +0,2 @@ | ||
import { ArcjetContext, ArcjetBotType, ArcjetEmailType, ArcjetMode, ArcjetStack, ArcjetDecision, ArcjetRule, ArcjetRequestDetails } from "@arcjet/protocol"; | ||
import { ArcjetContext, ArcjetBotType, ArcjetEmailType, ArcjetMode, ArcjetStack, ArcjetDecision, ArcjetRule, ArcjetRequestDetails, ArcjetLogger } from "@arcjet/protocol"; | ||
import { Transport } from "@arcjet/protocol/proto.js"; | ||
@@ -17,33 +17,9 @@ export * from "@arcjet/protocol"; | ||
export type RemoteClientOptions = { | ||
transport?: Transport; | ||
baseUrl?: string; | ||
timeout?: number; | ||
sdkStack?: ArcjetStack; | ||
sdkVersion?: string; | ||
transport: Transport; | ||
baseUrl: string; | ||
timeout: number; | ||
sdkStack: ArcjetStack; | ||
sdkVersion: string; | ||
}; | ||
export declare function defaultBaseUrl(): string; | ||
export declare function createRemoteClient(options?: RemoteClientOptions): RemoteClient; | ||
/** | ||
* Represents the runtime that the client is running in. This is used to bring | ||
* in the appropriate libraries for the runtime e.g. the WASM module. | ||
*/ | ||
export declare enum Runtime { | ||
/** | ||
* Running in a Node.js runtime | ||
*/ | ||
Node = "node", | ||
/** | ||
* Running in a Node.js runtime without WASM support e.g. Vercel serverless | ||
* functions | ||
* @see | ||
* https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js | ||
*/ | ||
Node_NoWASM = "node_nowasm", | ||
/** | ||
* Running in an Edge runtime | ||
* @see https://edge-runtime.vercel.app/ | ||
* @see https://vercel.com/docs/concepts/functions/edge-functions/edge-runtime | ||
*/ | ||
Edge = "edge" | ||
} | ||
export declare function createRemoteClient(options: RemoteClientOptions): RemoteClient; | ||
type TokenBucketRateLimitOptions<Characteristics extends readonly string[]> = { | ||
@@ -134,24 +110,2 @@ mode?: ArcjetMode; | ||
}; | ||
export declare class ArcjetHeaders extends Headers { | ||
constructor(init?: HeadersInit | Record<string, string | string[] | undefined>); | ||
/** | ||
* Append a key and value to the headers, while filtering any key named | ||
* `cookie`. | ||
* | ||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) | ||
* | ||
* @param key The key to append in the headers | ||
* @param value The value to append for the key in the headers | ||
*/ | ||
append(key: string, value: string): void; | ||
/** | ||
* Set a key and value in the headers, but filtering any key named `cookie`. | ||
* | ||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) | ||
* | ||
* @param key The key to set in the headers | ||
* @param value The value to set for the key in the headers | ||
*/ | ||
set(key: string, value: string): void; | ||
} | ||
type PlainObject = { | ||
@@ -166,2 +120,9 @@ [key: string]: unknown; | ||
/** | ||
* Additional context that can be provided by adapters. | ||
* | ||
* Among other things, this could include the Arcjet API Key if it were only | ||
* available in a runtime handler or IP details provided by a platform. | ||
*/ | ||
export type ArcjetAdapterContext = Record<string, unknown>; | ||
/** | ||
* @property {string} ip - The IP address of the client. | ||
@@ -225,2 +186,6 @@ * @property {string} method - The HTTP method of the request. | ||
client?: RemoteClient; | ||
/** | ||
* The logger used to emit useful information from the SDK. | ||
*/ | ||
log?: ArcjetLogger; | ||
} | ||
@@ -232,3 +197,2 @@ /** | ||
export interface Arcjet<Props extends PlainObject> { | ||
get runtime(): Runtime; | ||
/** | ||
@@ -238,6 +202,7 @@ * Make a decision about how to handle a request. This will analyze the | ||
* | ||
* @param {ArcjetAdapterContext} ctx - Additional context for this function call. | ||
* @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>; | ||
protect(ctx: ArcjetAdapterContext, request: ArcjetRequest<Props>): Promise<ArcjetDecision>; | ||
/** | ||
@@ -244,0 +209,0 @@ * Augments the client with another rule. Useful for varying rules based on |
303
index.js
@@ -7,3 +7,4 @@ import { ArcjetRuleResult, ArcjetEmailReason, ArcjetBotType, ArcjetErrorReason, ArcjetBotReason, ArcjetErrorDecision, ArcjetReason, ArcjetDenyDecision } from '@arcjet/protocol'; | ||
import * as duration from '@arcjet/duration'; | ||
import logger from '@arcjet/logger'; | ||
import ArcjetHeaders from '@arcjet/headers'; | ||
import { runtime } from '@arcjet/runtime'; | ||
@@ -15,5 +16,2 @@ function assert(condition, msg) { | ||
} | ||
function isIterable(val) { | ||
return typeof val?.[Symbol.iterator] === "function"; | ||
} | ||
function nowInSeconds() { | ||
@@ -63,39 +61,2 @@ return Math.floor(Date.now() / 1000); | ||
} | ||
const baseUrlAllowed = [ | ||
"https://decide.arcjet.com", | ||
"https://decide.arcjettest.com", | ||
"https://fly.decide.arcjet.com", | ||
"https://fly.decide.arcjettest.com", | ||
"https://decide.arcjet.orb.local:4082", | ||
]; | ||
function defaultBaseUrl() { | ||
// TODO(#90): Remove this production conditional before 1.0.0 | ||
if (process.env["NODE_ENV"] === "production") { | ||
// Use ARCJET_BASE_URL if it is set and belongs to our allowlist; otherwise | ||
// use the hardcoded default. | ||
if (typeof process.env["ARCJET_BASE_URL"] === "string" && | ||
baseUrlAllowed.includes(process.env["ARCJET_BASE_URL"])) { | ||
return process.env["ARCJET_BASE_URL"]; | ||
} | ||
// If we're running on fly.io, use the Arcjet Decide Service hosted on fly | ||
// Ref: https://fly.io/docs/machines/runtime-environment/#environment-variables | ||
if (typeof process.env["FLY_APP_NAME"] === "string" && | ||
process.env["FLY_APP_NAME"] !== "") { | ||
return "https://fly.decide.arcjet.com"; | ||
} | ||
return "https://decide.arcjet.com"; | ||
} | ||
else { | ||
if (process.env["ARCJET_BASE_URL"]) { | ||
return process.env["ARCJET_BASE_URL"]; | ||
} | ||
// If we're running on fly.io, use the Arcjet Decide Service hosted on fly | ||
// Ref: https://fly.io/docs/machines/runtime-environment/#environment-variables | ||
if (typeof process.env["FLY_APP_NAME"] === "string" && | ||
process.env["FLY_APP_NAME"] !== "") { | ||
return "https://fly.decide.arcjet.com"; | ||
} | ||
return "https://decide.arcjet.com"; | ||
} | ||
} | ||
const knownFields = [ | ||
@@ -138,18 +99,8 @@ "ip", | ||
function createRemoteClient(options) { | ||
// TODO(#207): Remove this when we can default the transport | ||
if (typeof options?.transport === "undefined") { | ||
throw new Error("Transport must be defined"); | ||
} | ||
// The base URL for the Arcjet API. Will default to the standard production | ||
// API unless environment variable `ARCJET_BASE_URL` is set. | ||
// TODO(#207): This is unused until we can default the transport | ||
const baseUrl = options?.baseUrl ?? defaultBaseUrl(); | ||
// 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 ?? (process.env["NODE_ENV"] === "production" ? 500 : 1000); | ||
const sdkStack = ArcjetStackToProtocol(options?.sdkStack ?? "NODEJS"); | ||
const sdkVersion = "1.0.0-alpha.13"; | ||
const client = createPromiseClient(DecideService, options.transport); | ||
const { transport, sdkVersion, baseUrl, timeout } = options; | ||
const sdkStack = ArcjetStackToProtocol(options.sdkStack); | ||
const client = createPromiseClient(DecideService, transport); | ||
return Object.freeze({ | ||
async decide(context, details, rules) { | ||
const { log } = context; | ||
// Build the request object from the Protobuf generated class. | ||
@@ -175,3 +126,3 @@ const decideRequest = new DecideRequest({ | ||
}); | ||
logger.debug("Decide request to %s", baseUrl); | ||
log.debug("Decide request to %s", baseUrl); | ||
const response = await client.decide(decideRequest, { | ||
@@ -182,7 +133,7 @@ headers: { Authorization: `Bearer ${context.key}` }, | ||
const decision = ArcjetDecisionFromProtocol(response.decision); | ||
logger.debug("Decide response", { | ||
log.debug({ | ||
id: decision.id, | ||
fingerprint: context.fingerprint, | ||
path: details.path, | ||
runtime: runtime(), | ||
runtime: context.runtime, | ||
ttl: decision.ttl, | ||
@@ -192,6 +143,7 @@ conclusion: decision.conclusion, | ||
ruleResults: decision.results, | ||
}); | ||
}, "Decide response"); | ||
return decision; | ||
}, | ||
report(context, details, decision, rules) { | ||
const { log } = context; | ||
// Build the request object from the Protobuf generated class. | ||
@@ -217,3 +169,3 @@ const reportRequest = new ReportRequest({ | ||
}); | ||
logger.debug("Report request to %s", baseUrl); | ||
log.debug("Report request to %s", baseUrl); | ||
// We use the promise API directly to avoid returning a promise from this function so execution can't be paused with `await` | ||
@@ -226,12 +178,12 @@ client | ||
.then((response) => { | ||
logger.debug("Report response", { | ||
log.debug({ | ||
id: response.decision?.id, | ||
fingerprint: context.fingerprint, | ||
path: details.path, | ||
runtime: runtime(), | ||
runtime: context.runtime, | ||
ttl: decision.ttl, | ||
}); | ||
}, "Report response"); | ||
}) | ||
.catch((err) => { | ||
logger.log("Encountered problem sending report: %s", errorMessage(err)); | ||
log.info("Encountered problem sending report: %s", errorMessage(err)); | ||
}); | ||
@@ -241,105 +193,2 @@ }, | ||
} | ||
/** | ||
* Represents the runtime that the client is running in. This is used to bring | ||
* in the appropriate libraries for the runtime e.g. the WASM module. | ||
*/ | ||
var Runtime; | ||
(function (Runtime) { | ||
/** | ||
* Running in a Node.js runtime | ||
*/ | ||
Runtime["Node"] = "node"; | ||
/** | ||
* Running in a Node.js runtime without WASM support e.g. Vercel serverless | ||
* functions | ||
* @see | ||
* https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js | ||
*/ | ||
Runtime["Node_NoWASM"] = "node_nowasm"; | ||
/** | ||
* Running in an Edge runtime | ||
* @see https://edge-runtime.vercel.app/ | ||
* @see https://vercel.com/docs/concepts/functions/edge-functions/edge-runtime | ||
*/ | ||
Runtime["Edge"] = "edge"; | ||
})(Runtime || (Runtime = {})); | ||
function runtime() { | ||
if (typeof process.env["ARCJET_RUNTIME"] === "string") { | ||
switch (process.env["ARCJET_RUNTIME"]) { | ||
case "edge": | ||
return Runtime.Edge; | ||
case "node": | ||
return Runtime.Node; | ||
case "node_nowasm": | ||
return Runtime.Node_NoWASM; | ||
default: | ||
throw new Error("Unknown ARCJET_RUNTIME specified!"); | ||
} | ||
} | ||
else { | ||
if (process.env["NEXT_RUNTIME"] === "edge") { | ||
return Runtime.Edge; | ||
} | ||
else if (process.env["VERCEL"] === "1") { | ||
return Runtime.Node_NoWASM; | ||
} | ||
else { | ||
return Runtime.Node; | ||
} | ||
} | ||
} | ||
class ArcjetHeaders extends Headers { | ||
constructor(init) { | ||
super(); | ||
if (typeof init !== "undefined") { | ||
if (isIterable(init)) { | ||
for (const [key, value] of init) { | ||
this.append(key, value); | ||
} | ||
} | ||
else { | ||
for (const [key, value] of Object.entries(init)) { | ||
if (typeof value === "undefined") { | ||
continue; | ||
} | ||
if (Array.isArray(value)) { | ||
for (const singleValue of value) { | ||
this.append(key, singleValue); | ||
} | ||
} | ||
else { | ||
this.append(key, value); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Append a key and value to the headers, while filtering any key named | ||
* `cookie`. | ||
* | ||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) | ||
* | ||
* @param key The key to append in the headers | ||
* @param value The value to append for the key in the headers | ||
*/ | ||
append(key, value) { | ||
if (key.toLowerCase() !== "cookie") { | ||
super.append(key, value); | ||
} | ||
} | ||
/** | ||
* Set a key and value in the headers, but filtering any key named `cookie`. | ||
* | ||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) | ||
* | ||
* @param key The key to set in the headers | ||
* @param value The value to set for the key in the headers | ||
*/ | ||
set(key, value) { | ||
if (key.toLowerCase() !== "cookie") { | ||
super.set(key, value); | ||
} | ||
} | ||
} | ||
const Priority = { | ||
@@ -468,3 +317,3 @@ Shield: 1, | ||
async protect(context, { email }) { | ||
if (await analyze.isValidEmail(email, analyzeOpts)) { | ||
if (await analyze.isValidEmail(context, email, analyzeOpts)) { | ||
return new ArcjetRuleResult({ | ||
@@ -555,3 +404,3 @@ ttl: 0, | ||
} | ||
const botResult = await analyze.detectBot(JSON.stringify(headersKV), JSON.stringify(Object.fromEntries(add.map(([key, botType]) => [ | ||
const botResult = await analyze.detectBot(context, JSON.stringify(headersKV), JSON.stringify(Object.fromEntries(add.map(([key, botType]) => [ | ||
key, | ||
@@ -641,11 +490,16 @@ ArcjetBotTypeToProtocol(botType), | ||
// We destructure here to make the function signature neat when viewed by consumers | ||
const { key, rules, client } = options; | ||
const { key, rules } = options; | ||
const rt = runtime(); | ||
// TODO: Separate the ArcjetOptions from the SDK Options | ||
// It is currently optional in the options so users can override it via an SDK | ||
if (typeof options.log === "undefined") { | ||
throw new Error("Log is required"); | ||
} | ||
const log = options.log; | ||
// TODO(#207): Remove this when we can default the transport so client is not required | ||
// It is currently optional in the options so the Next SDK can override it for the user | ||
if (typeof client === "undefined") { | ||
if (typeof options.client === "undefined") { | ||
throw new Error("Client is required"); | ||
} | ||
// This is reassigned to help TypeScript's type inference, as it loses the | ||
// type narrowing of the above `if` statement when using from inside `protect` | ||
const remoteClient = client; | ||
const client = options.client; | ||
// A local cache of block decisions. Might be emphemeral per request, | ||
@@ -658,3 +512,3 @@ // depending on the way the runtime works, but it's worth a try. | ||
.sort((a, b) => a.priority - b.priority); | ||
async function protect(rules, request) { | ||
async function protect(rules, ctx, request) { | ||
// This goes against the type definition above, but users might call | ||
@@ -679,4 +533,4 @@ // `protect()` with no value and we don't want to crash | ||
}); | ||
logger.time("local"); | ||
logger.time("fingerprint"); | ||
log.time?.("local"); | ||
log.time?.("fingerprint"); | ||
let ip = ""; | ||
@@ -687,14 +541,19 @@ if (typeof details.ip === "string") { | ||
if (details.ip === "") { | ||
logger.warn("generateFingerprint: ip is empty"); | ||
log.warn("generateFingerprint: ip is empty"); | ||
} | ||
const fingerprint = await analyze.generateFingerprint(ip); | ||
logger.debug("fingerprint (%s): %s", runtime(), fingerprint); | ||
logger.timeEnd("fingerprint"); | ||
const context = { key, fingerprint }; | ||
const baseContext = { | ||
key, | ||
log, | ||
...ctx, | ||
}; | ||
const fingerprint = await analyze.generateFingerprint(baseContext, ip); | ||
log.debug("fingerprint (%s): %s", rt, fingerprint); | ||
log.timeEnd?.("fingerprint"); | ||
const context = { ...baseContext, fingerprint, runtime: rt }; | ||
if (rules.length < 1) { | ||
// TODO(#607): Error if no rules configured after deprecation period | ||
logger.warn("Calling `protect()` with no rules is deprecated. Did you mean to configure the Shield rule?"); | ||
log.warn("Calling `protect()` with no rules is deprecated. Did you mean to configure the Shield rule?"); | ||
} | ||
if (rules.length > 10) { | ||
logger.error("Failure running rules. Only 10 rules may be specified."); | ||
log.error("Failure running rules. Only 10 rules may be specified."); | ||
const decision = new ArcjetErrorDecision({ | ||
@@ -707,3 +566,3 @@ ttl: 0, | ||
}); | ||
remoteClient.report(context, details, decision, | ||
client.report(context, details, decision, | ||
// No rules because we've determined they were too long and we don't | ||
@@ -728,13 +587,13 @@ // want to try to send them to the server | ||
// can take advantage of that. | ||
logger.time("cache"); | ||
log.time?.("cache"); | ||
const existingBlockReason = blockCache.get(fingerprint); | ||
logger.timeEnd("cache"); | ||
log.timeEnd?.("cache"); | ||
// If already blocked then we can async log to the API and return the | ||
// decision immediately. | ||
if (existingBlockReason) { | ||
logger.timeEnd("local"); | ||
logger.debug("decide: alreadyBlocked", { | ||
log.timeEnd?.("local"); | ||
log.debug({ | ||
fingerprint, | ||
existingBlockReason, | ||
}); | ||
}, "decide: alreadyBlocked"); | ||
const decision = new ArcjetDenyDecision({ | ||
@@ -746,4 +605,4 @@ ttl: blockCache.ttl(fingerprint), | ||
}); | ||
remoteClient.report(context, details, decision, rules); | ||
logger.debug("decide: already blocked", { | ||
client.report(context, details, decision, rules); | ||
log.debug({ | ||
id: decision.id, | ||
@@ -753,4 +612,4 @@ conclusion: decision.conclusion, | ||
reason: existingBlockReason, | ||
runtime: runtime(), | ||
}); | ||
runtime: rt, | ||
}, "decide: already blocked"); | ||
return decision; | ||
@@ -768,7 +627,7 @@ } | ||
} | ||
logger.time(rule.type); | ||
log.time?.(rule.type); | ||
try { | ||
localRule.validate(context, details); | ||
results[idx] = await localRule.protect(context, details); | ||
logger.debug("Local rule result:", { | ||
log.debug({ | ||
id: results[idx].ruleId, | ||
@@ -778,10 +637,10 @@ rule: rule.type, | ||
path: details.path, | ||
runtime: runtime(), | ||
runtime: rt, | ||
ttl: results[idx].ttl, | ||
conclusion: results[idx].conclusion, | ||
reason: results[idx].reason, | ||
}); | ||
}, "Local rule result:"); | ||
} | ||
catch (err) { | ||
logger.error("Failure running rule: %s due to %s", rule.type, errorMessage(err)); | ||
log.error("Failure running rule: %s due to %s", rule.type, errorMessage(err)); | ||
results[idx] = new ArcjetRuleResult({ | ||
@@ -794,5 +653,5 @@ ttl: 0, | ||
} | ||
logger.timeEnd(rule.type); | ||
log.timeEnd?.(rule.type); | ||
if (results[idx].isDenied()) { | ||
logger.timeEnd("local"); | ||
log.timeEnd?.("local"); | ||
const decision = new ArcjetDenyDecision({ | ||
@@ -806,3 +665,3 @@ ttl: results[idx].ttl, | ||
// request. | ||
remoteClient.report(context, details, decision, rules); | ||
client.report(context, details, decision, rules); | ||
// If we're not in DRY_RUN mode, we want to cache non-zero TTL results | ||
@@ -812,7 +671,7 @@ // and return this DENY decision. | ||
if (results[idx].ttl > 0) { | ||
logger.debug("Caching decision for %d seconds", decision.ttl, { | ||
log.debug({ | ||
fingerprint, | ||
conclusion: decision.conclusion, | ||
reason: decision.reason, | ||
}); | ||
}, "Caching decision for %d seconds", decision.ttl); | ||
blockCache.set(fingerprint, decision.reason, nowInSeconds() + decision.ttl); | ||
@@ -822,17 +681,17 @@ } | ||
} | ||
logger.warn(`Dry run mode is enabled for "%s" rule. Overriding decision. Decision was: %s`, rule.type, decision.conclusion); | ||
log.warn(`Dry run mode is enabled for "%s" rule. Overriding decision. Decision was: %s`, rule.type, decision.conclusion); | ||
} | ||
} | ||
logger.timeEnd("local"); | ||
logger.time("remote"); | ||
log.timeEnd?.("local"); | ||
log.time?.("remote"); | ||
// With no cached values, we take a decision remotely. We use a timeout to | ||
// fail open. | ||
try { | ||
logger.time("decideApi"); | ||
const decision = await remoteClient.decide(context, details, rules); | ||
logger.timeEnd("decideApi"); | ||
log.time?.("decideApi"); | ||
const decision = await client.decide(context, details, rules); | ||
log.timeEnd?.("decideApi"); | ||
// If the decision is to block and we have a non-zero TTL, we cache the | ||
// block locally | ||
if (decision.isDenied() && decision.ttl > 0) { | ||
logger.debug("decide: Caching block locally for %d seconds", decision.ttl); | ||
log.debug("decide: Caching block locally for %d seconds", decision.ttl); | ||
blockCache.set(fingerprint, decision.reason, nowInSeconds() + decision.ttl); | ||
@@ -843,3 +702,3 @@ } | ||
catch (err) { | ||
logger.error("Encountered problem getting remote decision: %s", errorMessage(err)); | ||
log.error("Encountered problem getting remote decision: %s", errorMessage(err)); | ||
const decision = new ArcjetErrorDecision({ | ||
@@ -850,7 +709,7 @@ ttl: 0, | ||
}); | ||
remoteClient.report(context, details, decision, rules); | ||
client.report(context, details, decision, rules); | ||
return decision; | ||
} | ||
finally { | ||
logger.timeEnd("remote"); | ||
log.timeEnd?.("remote"); | ||
} | ||
@@ -862,10 +721,7 @@ } | ||
return Object.freeze({ | ||
get runtime() { | ||
return runtime(); | ||
}, | ||
withRule(rule) { | ||
return withRule(rule); | ||
}, | ||
async protect(request) { | ||
return protect(rules, request); | ||
async protect(ctx, request) { | ||
return protect(rules, ctx, request); | ||
}, | ||
@@ -875,10 +731,7 @@ }); | ||
return Object.freeze({ | ||
get runtime() { | ||
return runtime(); | ||
}, | ||
withRule(rule) { | ||
return withRule(rule); | ||
}, | ||
async protect(request) { | ||
return protect(rootRules, request); | ||
async protect(ctx, request) { | ||
return protect(rootRules, ctx, request); | ||
}, | ||
@@ -888,2 +741,2 @@ }); | ||
export { ArcjetHeaders, Runtime, createRemoteClient, arcjet as default, defaultBaseUrl, detectBot, fixedWindow, protectSignup, rateLimit, shield, slidingWindow, tokenBucket, validateEmail }; | ||
export { createRemoteClient, arcjet as default, detectBot, fixedWindow, protectSignup, rateLimit, shield, slidingWindow, tokenBucket, validateEmail }; |
434
index.ts
@@ -24,2 +24,3 @@ import { | ||
ArcjetShieldRule, | ||
ArcjetLogger, | ||
} from "@arcjet/protocol"; | ||
@@ -43,3 +44,4 @@ import { | ||
import * as duration from "@arcjet/duration"; | ||
import logger from "@arcjet/logger"; | ||
import ArcjetHeaders from "@arcjet/headers"; | ||
import { runtime } from "@arcjet/runtime"; | ||
@@ -54,6 +56,2 @@ export * from "@arcjet/protocol"; | ||
function isIterable(val: any): val is Iterable<any> { | ||
return typeof val?.[Symbol.iterator] === "function"; | ||
} | ||
function nowInSeconds(): number { | ||
@@ -200,57 +198,9 @@ return Math.floor(Date.now() / 1000); | ||
export type RemoteClientOptions = { | ||
transport?: Transport; | ||
baseUrl?: string; | ||
timeout?: number; | ||
sdkStack?: ArcjetStack; | ||
sdkVersion?: string; | ||
transport: Transport; | ||
baseUrl: string; | ||
timeout: number; | ||
sdkStack: ArcjetStack; | ||
sdkVersion: string; | ||
}; | ||
const baseUrlAllowed = [ | ||
"https://decide.arcjet.com", | ||
"https://decide.arcjettest.com", | ||
"https://fly.decide.arcjet.com", | ||
"https://fly.decide.arcjettest.com", | ||
"https://decide.arcjet.orb.local:4082", | ||
]; | ||
export function defaultBaseUrl() { | ||
// TODO(#90): Remove this production conditional before 1.0.0 | ||
if (process.env["NODE_ENV"] === "production") { | ||
// Use ARCJET_BASE_URL if it is set and belongs to our allowlist; otherwise | ||
// use the hardcoded default. | ||
if ( | ||
typeof process.env["ARCJET_BASE_URL"] === "string" && | ||
baseUrlAllowed.includes(process.env["ARCJET_BASE_URL"]) | ||
) { | ||
return process.env["ARCJET_BASE_URL"]; | ||
} | ||
// If we're running on fly.io, use the Arcjet Decide Service hosted on fly | ||
// Ref: https://fly.io/docs/machines/runtime-environment/#environment-variables | ||
if ( | ||
typeof process.env["FLY_APP_NAME"] === "string" && | ||
process.env["FLY_APP_NAME"] !== "" | ||
) { | ||
return "https://fly.decide.arcjet.com"; | ||
} | ||
return "https://decide.arcjet.com"; | ||
} else { | ||
if (process.env["ARCJET_BASE_URL"]) { | ||
return process.env["ARCJET_BASE_URL"]; | ||
} | ||
// If we're running on fly.io, use the Arcjet Decide Service hosted on fly | ||
// Ref: https://fly.io/docs/machines/runtime-environment/#environment-variables | ||
if ( | ||
typeof process.env["FLY_APP_NAME"] === "string" && | ||
process.env["FLY_APP_NAME"] !== "" | ||
) { | ||
return "https://fly.decide.arcjet.com"; | ||
} | ||
return "https://decide.arcjet.com"; | ||
} | ||
} | ||
const knownFields = [ | ||
@@ -301,25 +251,9 @@ "ip", | ||
export function createRemoteClient( | ||
options?: RemoteClientOptions, | ||
): RemoteClient { | ||
// TODO(#207): Remove this when we can default the transport | ||
if (typeof options?.transport === "undefined") { | ||
throw new Error("Transport must be defined"); | ||
} | ||
export function createRemoteClient(options: RemoteClientOptions): RemoteClient { | ||
const { transport, sdkVersion, baseUrl, timeout } = options; | ||
// The base URL for the Arcjet API. Will default to the standard production | ||
// API unless environment variable `ARCJET_BASE_URL` is set. | ||
// TODO(#207): This is unused until we can default the transport | ||
const baseUrl = options?.baseUrl ?? defaultBaseUrl(); | ||
const sdkStack = ArcjetStackToProtocol(options.sdkStack); | ||
// 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 ?? (process.env["NODE_ENV"] === "production" ? 500 : 1000); | ||
const client = createPromiseClient(DecideService, transport); | ||
const sdkStack = ArcjetStackToProtocol(options?.sdkStack ?? "NODEJS"); | ||
const sdkVersion = "__ARCJET_SDK_VERSION__"; | ||
const client = createPromiseClient(DecideService, options.transport); | ||
return Object.freeze({ | ||
@@ -331,2 +265,4 @@ async decide( | ||
): Promise<ArcjetDecision> { | ||
const { log } = context; | ||
// Build the request object from the Protobuf generated class. | ||
@@ -353,3 +289,3 @@ const decideRequest = new DecideRequest({ | ||
logger.debug("Decide request to %s", baseUrl); | ||
log.debug("Decide request to %s", baseUrl); | ||
@@ -363,12 +299,15 @@ const response = await client.decide(decideRequest, { | ||
logger.debug("Decide response", { | ||
id: decision.id, | ||
fingerprint: context.fingerprint, | ||
path: details.path, | ||
runtime: runtime(), | ||
ttl: decision.ttl, | ||
conclusion: decision.conclusion, | ||
reason: decision.reason, | ||
ruleResults: decision.results, | ||
}); | ||
log.debug( | ||
{ | ||
id: decision.id, | ||
fingerprint: context.fingerprint, | ||
path: details.path, | ||
runtime: context.runtime, | ||
ttl: decision.ttl, | ||
conclusion: decision.conclusion, | ||
reason: decision.reason, | ||
ruleResults: decision.results, | ||
}, | ||
"Decide response", | ||
); | ||
@@ -384,2 +323,4 @@ return decision; | ||
): void { | ||
const { log } = context; | ||
// Build the request object from the Protobuf generated class. | ||
@@ -406,3 +347,3 @@ const reportRequest = new ReportRequest({ | ||
logger.debug("Report request to %s", baseUrl); | ||
log.debug("Report request to %s", baseUrl); | ||
@@ -416,15 +357,15 @@ // We use the promise API directly to avoid returning a promise from this function so execution can't be paused with `await` | ||
.then((response) => { | ||
logger.debug("Report response", { | ||
id: response.decision?.id, | ||
fingerprint: context.fingerprint, | ||
path: details.path, | ||
runtime: runtime(), | ||
ttl: decision.ttl, | ||
}); | ||
log.debug( | ||
{ | ||
id: response.decision?.id, | ||
fingerprint: context.fingerprint, | ||
path: details.path, | ||
runtime: context.runtime, | ||
ttl: decision.ttl, | ||
}, | ||
"Report response", | ||
); | ||
}) | ||
.catch((err: unknown) => { | ||
logger.log( | ||
"Encountered problem sending report: %s", | ||
errorMessage(err), | ||
); | ||
log.info("Encountered problem sending report: %s", errorMessage(err)); | ||
}); | ||
@@ -435,49 +376,2 @@ }, | ||
/** | ||
* Represents the runtime that the client is running in. This is used to bring | ||
* in the appropriate libraries for the runtime e.g. the WASM module. | ||
*/ | ||
export enum Runtime { | ||
/** | ||
* Running in a Node.js runtime | ||
*/ | ||
Node = "node", | ||
/** | ||
* Running in a Node.js runtime without WASM support e.g. Vercel serverless | ||
* functions | ||
* @see | ||
* https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js | ||
*/ | ||
Node_NoWASM = "node_nowasm", | ||
/** | ||
* Running in an Edge runtime | ||
* @see https://edge-runtime.vercel.app/ | ||
* @see https://vercel.com/docs/concepts/functions/edge-functions/edge-runtime | ||
*/ | ||
Edge = "edge", | ||
} | ||
function runtime(): Runtime { | ||
if (typeof process.env["ARCJET_RUNTIME"] === "string") { | ||
switch (process.env["ARCJET_RUNTIME"]) { | ||
case "edge": | ||
return Runtime.Edge; | ||
case "node": | ||
return Runtime.Node; | ||
case "node_nowasm": | ||
return Runtime.Node_NoWASM; | ||
default: | ||
throw new Error("Unknown ARCJET_RUNTIME specified!"); | ||
} | ||
} else { | ||
if (process.env["NEXT_RUNTIME"] === "edge") { | ||
return Runtime.Edge; | ||
} else if (process.env["VERCEL"] === "1") { | ||
return Runtime.Node_NoWASM; | ||
} else { | ||
return Runtime.Node; | ||
} | ||
} | ||
} | ||
type TokenBucketRateLimitOptions<Characteristics extends readonly string[]> = { | ||
@@ -572,61 +466,2 @@ mode?: ArcjetMode; | ||
export class ArcjetHeaders extends Headers { | ||
constructor( | ||
init?: HeadersInit | Record<string, string | string[] | undefined>, | ||
) { | ||
super(); | ||
if (typeof init !== "undefined") { | ||
if (isIterable(init)) { | ||
for (const [key, value] of init) { | ||
this.append(key, value); | ||
} | ||
} else { | ||
for (const [key, value] of Object.entries( | ||
init as Record<string, string | string[] | undefined>, | ||
)) { | ||
if (typeof value === "undefined") { | ||
continue; | ||
} | ||
if (Array.isArray(value)) { | ||
for (const singleValue of value) { | ||
this.append(key, singleValue); | ||
} | ||
} else { | ||
this.append(key, value); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Append a key and value to the headers, while filtering any key named | ||
* `cookie`. | ||
* | ||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) | ||
* | ||
* @param key The key to append in the headers | ||
* @param value The value to append for the key in the headers | ||
*/ | ||
append(key: string, value: string): void { | ||
if (key.toLowerCase() !== "cookie") { | ||
super.append(key, value); | ||
} | ||
} | ||
/** | ||
* Set a key and value in the headers, but filtering any key named `cookie`. | ||
* | ||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) | ||
* | ||
* @param key The key to set in the headers | ||
* @param value The value to set for the key in the headers | ||
*/ | ||
set(key: string, value: string): void { | ||
if (key.toLowerCase() !== "cookie") { | ||
super.set(key, value); | ||
} | ||
} | ||
} | ||
const Priority = { | ||
@@ -681,2 +516,10 @@ Shield: 1, | ||
/** | ||
* Additional context that can be provided by adapters. | ||
* | ||
* Among other things, this could include the Arcjet API Key if it were only | ||
* available in a runtime handler or IP details provided by a platform. | ||
*/ | ||
export type ArcjetAdapterContext = Record<string, unknown>; | ||
/** | ||
* @property {string} ip - The IP address of the client. | ||
@@ -895,3 +738,3 @@ * @property {string} method - The HTTP method of the request. | ||
): Promise<ArcjetRuleResult> { | ||
if (await analyze.isValidEmail(email, analyzeOpts)) { | ||
if (await analyze.isValidEmail(context, email, analyzeOpts)) { | ||
return new ArcjetRuleResult({ | ||
@@ -1003,2 +846,3 @@ ttl: 0, | ||
const botResult = await analyze.detectBot( | ||
context, | ||
JSON.stringify(headersKV), | ||
@@ -1133,2 +977,6 @@ JSON.stringify( | ||
client?: RemoteClient; | ||
/** | ||
* The logger used to emit useful information from the SDK. | ||
*/ | ||
log?: ArcjetLogger; | ||
} | ||
@@ -1141,3 +989,2 @@ | ||
export interface Arcjet<Props extends PlainObject> { | ||
get runtime(): Runtime; | ||
/** | ||
@@ -1147,6 +994,10 @@ * Make a decision about how to handle a request. This will analyze the | ||
* | ||
* @param {ArcjetAdapterContext} ctx - Additional context for this function call. | ||
* @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>; | ||
protect( | ||
ctx: ArcjetAdapterContext, | ||
request: ArcjetRequest<Props>, | ||
): Promise<ArcjetDecision>; | ||
@@ -1174,12 +1025,19 @@ /** | ||
// We destructure here to make the function signature neat when viewed by consumers | ||
const { key, rules, client } = options; | ||
const { key, rules } = options; | ||
const rt = runtime(); | ||
// TODO: Separate the ArcjetOptions from the SDK Options | ||
// It is currently optional in the options so users can override it via an SDK | ||
if (typeof options.log === "undefined") { | ||
throw new Error("Log is required"); | ||
} | ||
const log = options.log; | ||
// TODO(#207): Remove this when we can default the transport so client is not required | ||
// It is currently optional in the options so the Next SDK can override it for the user | ||
if (typeof client === "undefined") { | ||
if (typeof options.client === "undefined") { | ||
throw new Error("Client is required"); | ||
} | ||
// This is reassigned to help TypeScript's type inference, as it loses the | ||
// type narrowing of the above `if` statement when using from inside `protect` | ||
const remoteClient = client; | ||
const client = options.client; | ||
@@ -1197,2 +1055,3 @@ // A local cache of block decisions. Might be emphemeral per request, | ||
rules: ArcjetRule[], | ||
ctx: ArcjetAdapterContext, | ||
request: ArcjetRequest<Props>, | ||
@@ -1221,5 +1080,5 @@ ) { | ||
logger.time("local"); | ||
log.time?.("local"); | ||
logger.time("fingerprint"); | ||
log.time?.("fingerprint"); | ||
let ip = ""; | ||
@@ -1230,13 +1089,20 @@ if (typeof details.ip === "string") { | ||
if (details.ip === "") { | ||
logger.warn("generateFingerprint: ip is empty"); | ||
log.warn("generateFingerprint: ip is empty"); | ||
} | ||
const fingerprint = await analyze.generateFingerprint(ip); | ||
logger.debug("fingerprint (%s): %s", runtime(), fingerprint); | ||
logger.timeEnd("fingerprint"); | ||
const context: ArcjetContext = { key, fingerprint }; | ||
const baseContext = { | ||
key, | ||
log, | ||
...ctx, | ||
}; | ||
const fingerprint = await analyze.generateFingerprint(baseContext, ip); | ||
log.debug("fingerprint (%s): %s", rt, fingerprint); | ||
log.timeEnd?.("fingerprint"); | ||
const context: ArcjetContext = { ...baseContext, fingerprint, runtime: rt }; | ||
if (rules.length < 1) { | ||
// TODO(#607): Error if no rules configured after deprecation period | ||
logger.warn( | ||
log.warn( | ||
"Calling `protect()` with no rules is deprecated. Did you mean to configure the Shield rule?", | ||
@@ -1247,3 +1113,3 @@ ); | ||
if (rules.length > 10) { | ||
logger.error("Failure running rules. Only 10 rules may be specified."); | ||
log.error("Failure running rules. Only 10 rules may be specified."); | ||
@@ -1258,3 +1124,3 @@ const decision = new ArcjetErrorDecision({ | ||
remoteClient.report( | ||
client.report( | ||
context, | ||
@@ -1286,5 +1152,5 @@ details, | ||
// can take advantage of that. | ||
logger.time("cache"); | ||
log.time?.("cache"); | ||
const existingBlockReason = blockCache.get(fingerprint); | ||
logger.timeEnd("cache"); | ||
log.timeEnd?.("cache"); | ||
@@ -1294,7 +1160,10 @@ // If already blocked then we can async log to the API and return the | ||
if (existingBlockReason) { | ||
logger.timeEnd("local"); | ||
logger.debug("decide: alreadyBlocked", { | ||
fingerprint, | ||
existingBlockReason, | ||
}); | ||
log.timeEnd?.("local"); | ||
log.debug( | ||
{ | ||
fingerprint, | ||
existingBlockReason, | ||
}, | ||
"decide: alreadyBlocked", | ||
); | ||
const decision = new ArcjetDenyDecision({ | ||
@@ -1307,11 +1176,14 @@ ttl: blockCache.ttl(fingerprint), | ||
remoteClient.report(context, details, decision, rules); | ||
client.report(context, details, decision, rules); | ||
logger.debug("decide: already blocked", { | ||
id: decision.id, | ||
conclusion: decision.conclusion, | ||
fingerprint, | ||
reason: existingBlockReason, | ||
runtime: runtime(), | ||
}); | ||
log.debug( | ||
{ | ||
id: decision.id, | ||
conclusion: decision.conclusion, | ||
fingerprint, | ||
reason: existingBlockReason, | ||
runtime: rt, | ||
}, | ||
"decide: already blocked", | ||
); | ||
@@ -1331,3 +1203,3 @@ return decision; | ||
logger.time(rule.type); | ||
log.time?.(rule.type); | ||
@@ -1338,14 +1210,17 @@ try { | ||
logger.debug("Local rule result:", { | ||
id: results[idx].ruleId, | ||
rule: rule.type, | ||
fingerprint, | ||
path: details.path, | ||
runtime: runtime(), | ||
ttl: results[idx].ttl, | ||
conclusion: results[idx].conclusion, | ||
reason: results[idx].reason, | ||
}); | ||
log.debug( | ||
{ | ||
id: results[idx].ruleId, | ||
rule: rule.type, | ||
fingerprint, | ||
path: details.path, | ||
runtime: rt, | ||
ttl: results[idx].ttl, | ||
conclusion: results[idx].conclusion, | ||
reason: results[idx].reason, | ||
}, | ||
"Local rule result:", | ||
); | ||
} catch (err) { | ||
logger.error( | ||
log.error( | ||
"Failure running rule: %s due to %s", | ||
@@ -1364,6 +1239,6 @@ rule.type, | ||
logger.timeEnd(rule.type); | ||
log.timeEnd?.(rule.type); | ||
if (results[idx].isDenied()) { | ||
logger.timeEnd("local"); | ||
log.timeEnd?.("local"); | ||
@@ -1379,3 +1254,3 @@ const decision = new ArcjetDenyDecision({ | ||
// request. | ||
remoteClient.report(context, details, decision, rules); | ||
client.report(context, details, decision, rules); | ||
@@ -1386,7 +1261,11 @@ // If we're not in DRY_RUN mode, we want to cache non-zero TTL results | ||
if (results[idx].ttl > 0) { | ||
logger.debug("Caching decision for %d seconds", decision.ttl, { | ||
fingerprint, | ||
conclusion: decision.conclusion, | ||
reason: decision.reason, | ||
}); | ||
log.debug( | ||
{ | ||
fingerprint, | ||
conclusion: decision.conclusion, | ||
reason: decision.reason, | ||
}, | ||
"Caching decision for %d seconds", | ||
decision.ttl, | ||
); | ||
@@ -1403,3 +1282,3 @@ blockCache.set( | ||
logger.warn( | ||
log.warn( | ||
`Dry run mode is enabled for "%s" rule. Overriding decision. Decision was: %s`, | ||
@@ -1412,4 +1291,4 @@ rule.type, | ||
logger.timeEnd("local"); | ||
logger.time("remote"); | ||
log.timeEnd?.("local"); | ||
log.time?.("remote"); | ||
@@ -1419,5 +1298,5 @@ // With no cached values, we take a decision remotely. We use a timeout to | ||
try { | ||
logger.time("decideApi"); | ||
const decision = await remoteClient.decide(context, details, rules); | ||
logger.timeEnd("decideApi"); | ||
log.time?.("decideApi"); | ||
const decision = await client.decide(context, details, rules); | ||
log.timeEnd?.("decideApi"); | ||
@@ -1427,6 +1306,3 @@ // If the decision is to block and we have a non-zero TTL, we cache the | ||
if (decision.isDenied() && decision.ttl > 0) { | ||
logger.debug( | ||
"decide: Caching block locally for %d seconds", | ||
decision.ttl, | ||
); | ||
log.debug("decide: Caching block locally for %d seconds", decision.ttl); | ||
@@ -1442,3 +1318,3 @@ blockCache.set( | ||
} catch (err) { | ||
logger.error( | ||
log.error( | ||
"Encountered problem getting remote decision: %s", | ||
@@ -1453,7 +1329,7 @@ errorMessage(err), | ||
remoteClient.report(context, details, decision, rules); | ||
client.report(context, details, decision, rules); | ||
return decision; | ||
} finally { | ||
logger.timeEnd("remote"); | ||
log.timeEnd?.("remote"); | ||
} | ||
@@ -1469,5 +1345,2 @@ } | ||
return Object.freeze({ | ||
get runtime() { | ||
return runtime(); | ||
}, | ||
withRule(rule: Primitive | Product) { | ||
@@ -1477,5 +1350,6 @@ return withRule(rule); | ||
async protect( | ||
ctx: ArcjetAdapterContext, | ||
request: ArcjetRequest<ExtraProps<typeof rules>>, | ||
): Promise<ArcjetDecision> { | ||
return protect(rules, request); | ||
return protect(rules, ctx, request); | ||
}, | ||
@@ -1486,5 +1360,2 @@ }); | ||
return Object.freeze({ | ||
get runtime() { | ||
return runtime(); | ||
}, | ||
withRule(rule: Primitive | Product) { | ||
@@ -1494,7 +1365,8 @@ return withRule(rule); | ||
async protect( | ||
ctx: ArcjetAdapterContext, | ||
request: ArcjetRequest<ExtraProps<typeof rootRules>>, | ||
): Promise<ArcjetDecision> { | ||
return protect(rootRules, request); | ||
return protect(rootRules, ctx, request); | ||
}, | ||
}); | ||
} |
{ | ||
"name": "arcjet", | ||
"version": "1.0.0-alpha.13", | ||
"version": "1.0.0-alpha.14", | ||
"description": "Arcjet TypeScript and JavaScript SDK core", | ||
@@ -43,14 +43,16 @@ "license": "Apache-2.0", | ||
"dependencies": { | ||
"@arcjet/analyze": "1.0.0-alpha.13", | ||
"@arcjet/duration": "1.0.0-alpha.13", | ||
"@arcjet/logger": "1.0.0-alpha.13", | ||
"@arcjet/protocol": "1.0.0-alpha.13" | ||
"@arcjet/analyze": "1.0.0-alpha.14", | ||
"@arcjet/duration": "1.0.0-alpha.14", | ||
"@arcjet/headers": "1.0.0-alpha.14", | ||
"@arcjet/logger": "1.0.0-alpha.14", | ||
"@arcjet/protocol": "1.0.0-alpha.14", | ||
"@arcjet/runtime": "1.0.0-alpha.14" | ||
}, | ||
"devDependencies": { | ||
"@arcjet/eslint-config": "1.0.0-alpha.13", | ||
"@arcjet/rollup-config": "1.0.0-alpha.13", | ||
"@arcjet/tsconfig": "1.0.0-alpha.13", | ||
"@arcjet/eslint-config": "1.0.0-alpha.14", | ||
"@arcjet/rollup-config": "1.0.0-alpha.14", | ||
"@arcjet/tsconfig": "1.0.0-alpha.14", | ||
"@edge-runtime/jest-environment": "2.3.10", | ||
"@jest/globals": "29.7.0", | ||
"@rollup/wasm-node": "4.17.2", | ||
"@rollup/wasm-node": "4.18.0", | ||
"@types/node": "18.18.0", | ||
@@ -57,0 +59,0 @@ "jest": "29.7.0", |
<a href="https://arcjet.com" target="_arcjet-home"> | ||
<picture> | ||
<source media="(prefers-color-scheme: dark)" srcset="https://arcjet.com/arcjet-logo-minimal-dark-mark-all.svg"> | ||
<img src="https://arcjet.com/arcjet-logo-minimal-light-mark-all.svg" alt="Arcjet Logo" height="128" width="auto"> | ||
<source media="(prefers-color-scheme: dark)" srcset="https://arcjet.com/logo/arcjet-dark-lockup-voyage-horizontal.svg"> | ||
<img src="https://arcjet.com/logo/arcjet-light-lockup-voyage-horizontal.svg" alt="Arcjet Logo" height="128" width="auto"> | ||
</picture> | ||
@@ -20,3 +20,3 @@ </a> | ||
[Arcjet][arcjet] helps developers protect their apps in just a few lines of | ||
code. Implement rate limiting, bot protection, email verification & defend | ||
code. Implement rate limiting, bot protection, email verification, and defense | ||
against common attacks. | ||
@@ -45,3 +45,4 @@ | ||
import http from "http"; | ||
import arcjet, { createRemoteClient, defaultBaseUrl } from "arcjet"; | ||
import arcjet, { createRemoteClient } from "arcjet"; | ||
import { baseUrl } from "@arcjet/env"; | ||
import { createConnectTransport } from "@connectrpc/connect-node"; | ||
@@ -57,3 +58,3 @@ | ||
transport: createConnectTransport({ | ||
baseUrl: defaultBaseUrl(), | ||
baseUrl: baseUrl(process.env), | ||
httpVersion: "2", | ||
@@ -68,2 +69,6 @@ }), | ||
) { | ||
// Any sort of additional context that might want to be included for the | ||
// execution of `protect()`. This is mostly only useful for writing adapters. | ||
const ctx = {}; | ||
// Construct an object with Arcjet request details | ||
@@ -78,3 +83,3 @@ const path = new URL(req.url || "", `http://${req.headers.host}`); | ||
const decision = await aj.protect(details); | ||
const decision = await aj.protect(ctx, details); | ||
console.log(decision); | ||
@@ -81,0 +86,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 6 instances in 1 package
105
0
91967
6
2115
+ Added@arcjet/analyze@1.0.0-alpha.14(transitive)
+ Added@arcjet/headers@1.0.0-alpha.14(transitive)
+ Added@arcjet/logger@1.0.0-alpha.14(transitive)
+ Added@arcjet/protocol@1.0.0-alpha.14(transitive)
+ Added@arcjet/runtime@1.0.0-alpha.14(transitive)
+ Added@arcjet/sprintf@1.0.0-alpha.14(transitive)
+ Added@bufbuild/protobuf@1.10.0(transitive)
- Removed@arcjet/analyze@1.0.0-alpha.13(transitive)
- Removed@arcjet/logger@1.0.0-alpha.13(transitive)
- Removed@arcjet/protocol@1.0.0-alpha.13(transitive)
- Removed@bufbuild/protobuf@1.9.0(transitive)