Skip to content

Commit

Permalink
feat: support allow or deny config in validateEmail & deprecate `…
Browse files Browse the repository at this point in the history
…block` config (#2661)

This PR updates the `validateEmail` rule to support the allow/deny config.

Closes #1834
  • Loading branch information
e-moran authored Jan 14, 2025
1 parent cff2e3a commit 890afcd
Show file tree
Hide file tree
Showing 17 changed files with 409 additions and 79 deletions.
Binary file modified analyze-wasm/wasm/arcjet_analyze_js_req.component.core.wasm
Binary file not shown.
18 changes: 16 additions & 2 deletions analyze-wasm/wasm/arcjet_analyze_js_req.component.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,24 @@ export interface EmailValidationResult {
validity: EmailValidity,
blocked: Array<string>,
}
export interface EmailValidationConfig {
export interface AllowEmailValidationConfig {
requireTopLevelDomain: boolean,
allowDomainLiteral: boolean,
blockedEmails: Array<string>,
allow: Array<string>,
}
export interface DenyEmailValidationConfig {
requireTopLevelDomain: boolean,
allowDomainLiteral: boolean,
deny: Array<string>,
}
export type EmailValidationConfig = EmailValidationConfigAllowEmailValidationConfig | EmailValidationConfigDenyEmailValidationConfig;
export interface EmailValidationConfigAllowEmailValidationConfig {
tag: 'allow-email-validation-config',
val: AllowEmailValidationConfig,
}
export interface EmailValidationConfigDenyEmailValidationConfig {
tag: 'deny-email-validation-config',
val: DenyEmailValidationConfig,
}
export type SensitiveInfoEntities = SensitiveInfoEntitiesAllow | SensitiveInfoEntitiesDeny;
export interface SensitiveInfoEntitiesAllow {
Expand Down
106 changes: 73 additions & 33 deletions analyze-wasm/wasm/arcjet_analyze_js_req.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,69 +514,109 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
function isValidEmail(arg0, arg1) {
var ptr0 = utf8Encode(arg0, realloc0, memory0);
var len0 = utf8EncodedLen;
var {requireTopLevelDomain: v1_0, allowDomainLiteral: v1_1, blockedEmails: v1_2 } = arg1;
var vec3 = v1_2;
var len3 = vec3.length;
var result3 = realloc0(0, 0, 4, len3 * 8);
for (let i = 0; i < vec3.length; i++) {
const e = vec3[i];
const base = result3 + i * 8;var ptr2 = utf8Encode(e, realloc0, memory0);
var len2 = utf8EncodedLen;
dataView(memory0).setInt32(base + 4, len2, true);
dataView(memory0).setInt32(base + 0, ptr2, true);
var variant7 = arg1;
let variant7_0;
let variant7_1;
let variant7_2;
let variant7_3;
let variant7_4;
switch (variant7.tag) {
case 'allow-email-validation-config': {
const e = variant7.val;
var {requireTopLevelDomain: v1_0, allowDomainLiteral: v1_1, allow: v1_2 } = e;
var vec3 = v1_2;
var len3 = vec3.length;
var result3 = realloc0(0, 0, 4, len3 * 8);
for (let i = 0; i < vec3.length; i++) {
const e = vec3[i];
const base = result3 + i * 8;var ptr2 = utf8Encode(e, realloc0, memory0);
var len2 = utf8EncodedLen;
dataView(memory0).setInt32(base + 4, len2, true);
dataView(memory0).setInt32(base + 0, ptr2, true);
}
variant7_0 = 0;
variant7_1 = v1_0 ? 1 : 0;
variant7_2 = v1_1 ? 1 : 0;
variant7_3 = result3;
variant7_4 = len3;
break;
}
case 'deny-email-validation-config': {
const e = variant7.val;
var {requireTopLevelDomain: v4_0, allowDomainLiteral: v4_1, deny: v4_2 } = e;
var vec6 = v4_2;
var len6 = vec6.length;
var result6 = realloc0(0, 0, 4, len6 * 8);
for (let i = 0; i < vec6.length; i++) {
const e = vec6[i];
const base = result6 + i * 8;var ptr5 = utf8Encode(e, realloc0, memory0);
var len5 = utf8EncodedLen;
dataView(memory0).setInt32(base + 4, len5, true);
dataView(memory0).setInt32(base + 0, ptr5, true);
}
variant7_0 = 1;
variant7_1 = v4_0 ? 1 : 0;
variant7_2 = v4_1 ? 1 : 0;
variant7_3 = result6;
variant7_4 = len6;
break;
}
default: {
throw new TypeError(`invalid variant tag value \`${JSON.stringify(variant7.tag)}\` (received \`${variant7}\`) specified for \`EmailValidationConfig\``);
}
}
const ret = exports1['is-valid-email'](ptr0, len0, v1_0 ? 1 : 0, v1_1 ? 1 : 0, result3, len3);
let variant8;
const ret = exports1['is-valid-email'](ptr0, len0, variant7_0, variant7_1, variant7_2, variant7_3, variant7_4);
let variant12;
switch (dataView(memory0).getUint8(ret + 0, true)) {
case 0: {
let enum4;
let enum8;
switch (dataView(memory0).getUint8(ret + 4, true)) {
case 0: {
enum4 = 'valid';
enum8 = 'valid';
break;
}
case 1: {
enum4 = 'invalid';
enum8 = 'invalid';
break;
}
default: {
throw new TypeError('invalid discriminant specified for EmailValidity');
}
}
var len6 = dataView(memory0).getInt32(ret + 12, true);
var base6 = dataView(memory0).getInt32(ret + 8, true);
var result6 = [];
for (let i = 0; i < len6; i++) {
const base = base6 + i * 8;
var ptr5 = dataView(memory0).getInt32(base + 0, true);
var len5 = dataView(memory0).getInt32(base + 4, true);
var result5 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr5, len5));
result6.push(result5);
var len10 = dataView(memory0).getInt32(ret + 12, true);
var base10 = dataView(memory0).getInt32(ret + 8, true);
var result10 = [];
for (let i = 0; i < len10; i++) {
const base = base10 + i * 8;
var ptr9 = dataView(memory0).getInt32(base + 0, true);
var len9 = dataView(memory0).getInt32(base + 4, true);
var result9 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr9, len9));
result10.push(result9);
}
variant8= {
variant12= {
tag: 'ok',
val: {
validity: enum4,
blocked: result6,
validity: enum8,
blocked: result10,
}
};
break;
}
case 1: {
var ptr7 = dataView(memory0).getInt32(ret + 4, true);
var len7 = dataView(memory0).getInt32(ret + 8, true);
var result7 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr7, len7));
variant8= {
var ptr11 = dataView(memory0).getInt32(ret + 4, true);
var len11 = dataView(memory0).getInt32(ret + 8, true);
var result11 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr11, len11));
variant12= {
tag: 'err',
val: result7
val: result11
};
break;
}
default: {
throw new TypeError('invalid variant discriminant for expected');
}
}
const retVal = variant8;
const retVal = variant12;
postReturn3(ret);
if (typeof retVal === 'object' && retVal.tag === 'err') {
throw new ComponentError(retVal.val);
Expand Down
Binary file modified analyze-wasm/wasm/arcjet_analyze_js_req.component.wasm
Binary file not shown.
10 changes: 2 additions & 8 deletions analyze/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,14 @@ export async function generateFingerprint(
export async function isValidEmail(
context: AnalyzeContext,
candidate: string,
options?: EmailValidationConfig,
options: EmailValidationConfig,
): Promise<EmailValidationResult> {
const { log } = context;
const coreImports = createCoreImports();
const analyze = await initializeWasm(coreImports);
const optionsOrDefault = {
requireTopLevelDomain: true,
allowDomainLiteral: false,
blockedEmails: [],
...options,
};

if (typeof analyze !== "undefined") {
return analyze.isValidEmail(candidate, optionsOrDefault);
return analyze.isValidEmail(candidate, options);
} else {
log.debug("WebAssembly is not supported in this runtime");
// Skip the local evaluation of the rule if WASM is not available
Expand Down
119 changes: 109 additions & 10 deletions arcjet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
DetectedSensitiveInfoEntity,
SensitiveInfoEntity,
BotConfig,
EmailValidationConfig,
} from "@arcjet/analyze";
import * as duration from "@arcjet/duration";
import ArcjetHeaders from "@arcjet/headers";
Expand Down Expand Up @@ -461,6 +462,8 @@ const validateEmailOptions = createValidator({
validations: [
{ key: "mode", required: false, validate: validateMode },
{ key: "block", required: false, validate: validateEmailTypes },
{ key: "allow", required: false, validate: validateEmailTypes },
{ key: "deny", required: false, validate: validateEmailTypes },
{
key: "requireTopLevelDomain",
required: false,
Expand Down Expand Up @@ -526,13 +529,39 @@ export type BotOptionsDeny = {

export type BotOptions = BotOptionsAllow | BotOptionsDeny;

export type EmailOptions = {
export type EmailOptionsAllow = {
mode?: ArcjetMode;
block?: ArcjetEmailType[];
allow: ArcjetEmailType[];
block?: never;
deny?: never;
requireTopLevelDomain?: boolean;
allowDomainLiteral?: boolean;
};

export type EmailOptionsDeny = {
mode?: ArcjetMode;
allow?: never;
block?: never;
deny: ArcjetEmailType[];
requireTopLevelDomain?: boolean;
allowDomainLiteral?: boolean;
};

type EmailOptionsBlock = {
mode?: ArcjetMode;
allow?: never;
/** @deprecated use `deny` instead */
block: ArcjetEmailType[];
deny?: never;
requireTopLevelDomain?: boolean;
allowDomainLiteral?: boolean;
};

export type EmailOptions =
| EmailOptionsAllow
| EmailOptionsDeny
| EmailOptionsBlock;

type DetectSensitiveInfoEntities<T> = (
tokens: string[],
) => Array<ArcjetSensitiveInfoType | T | undefined>;
Expand Down Expand Up @@ -949,24 +978,94 @@ export function validateEmail(
options: EmailOptions,
): Primitive<{ email: string }> {
validateEmailOptions(options);

const mode = options.mode === "LIVE" ? "LIVE" : "DRY_RUN";
const block = options.block ?? [];
if (
typeof options.allow !== "undefined" &&
typeof options.deny !== "undefined"
) {
throw new Error(
"`validateEmail` options error: `allow` and `deny` cannot be provided together",
);
}
if (
typeof options.allow !== "undefined" &&
typeof options.block !== "undefined"
) {
throw new Error(
"`validateEmail` options error: `allow` and `block` cannot be provided together",
);
}
if (
typeof options.deny !== "undefined" &&
typeof options.block !== "undefined"
) {
throw new Error(
"`validateEmail` options error: `deny` and `block` cannot be provided together, `block` is now deprecated so `deny` should be preferred.",
);
}
if (
typeof options.allow === "undefined" &&
typeof options.deny === "undefined" &&
typeof options.block === "undefined"
) {
throw new Error(
"`validateEmail` options error: either `allow` or `deny` must be specified",
);
}
const allow = options.allow ?? [];
const deny = options.deny ?? options.block ?? [];
const requireTopLevelDomain = options.requireTopLevelDomain ?? true;
const allowDomainLiteral = options.allowDomainLiteral ?? false;

const emailOpts = {
requireTopLevelDomain,
allowDomainLiteral,
blockedEmails: block,
let config: EmailValidationConfig = {
tag: "deny-email-validation-config",
val: {
requireTopLevelDomain,
allowDomainLiteral,
deny: [],
},
};

if (typeof options.allow !== "undefined") {
config = {
tag: "allow-email-validation-config",
val: {
requireTopLevelDomain,
allowDomainLiteral,
allow: options.allow,
},
};
}

if (typeof options.deny !== "undefined") {
config = {
tag: "deny-email-validation-config",
val: {
requireTopLevelDomain,
allowDomainLiteral,
deny: options.deny,
},
};
}

if (typeof options.block !== "undefined") {
config = {
tag: "deny-email-validation-config",
val: {
requireTopLevelDomain,
allowDomainLiteral,
deny: options.block,
},
};
}

return [
<ArcjetEmailRule<{ email: string }>>{
type: "EMAIL",
priority: Priority.EmailValidation,
mode,
block,
allow,
deny,
requireTopLevelDomain,
allowDomainLiteral,

Expand All @@ -984,7 +1083,7 @@ export function validateEmail(
context: ArcjetContext,
{ email }: ArcjetRequestDetails & { email: string },
): Promise<ArcjetRuleResult> {
const result = await analyze.isValidEmail(context, email, emailOpts);
const result = await analyze.isValidEmail(context, email, config);
if (result.validity === "valid") {
return new ArcjetRuleResult({
ttl: 0,
Expand Down
Loading

0 comments on commit 890afcd

Please sign in to comment.