146 lines
7.0 KiB
JavaScript
146 lines
7.0 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
|
|
import { validateReleaseCandidateRecord } from "./release-candidate.mjs";
|
|
|
|
export const RESEND_DAILY_LIMIT = 80;
|
|
export const RESEND_MONTHLY_LIMIT = 2_400;
|
|
export const DELIVERY_CATEGORIES = Object.freeze(["qq", "163", "enterprise"]);
|
|
export const DELIVERY_SAMPLE_SIZE = 20;
|
|
export const DELIVERY_MINIMUM_WITHIN_TWO_MINUTES = 19;
|
|
export const DELIVERY_WINDOW_SECONDS = 120;
|
|
export const EXPECTED_WP7_01_COMMIT = "623cad25b2a2a9a003502c9a92ebd318dad06248";
|
|
|
|
const emailPattern = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
|
|
const absolutePathPattern = /(?:[A-Z]:[\\/]|\\\\|\/Users\/|\/home\/)/i;
|
|
const sensitiveKeyPattern = /"(?:api[_ -]?key|secret|password|authorization|bearer|cookie|session[_ -]?token|verification[_ -]?code|private[_ -]?content|prompt|image)"\s*:/i;
|
|
|
|
function object(value) {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
}
|
|
|
|
function errorList(...values) {
|
|
return [...new Set(values.flat().filter((value) => typeof value === "string" && value.length > 0))];
|
|
}
|
|
|
|
export function validateDomainCheck(value) {
|
|
const item = object(value);
|
|
const freeRules = object(item?.free_rules);
|
|
const spf = object(item?.spf);
|
|
const dkim = object(item?.dkim);
|
|
const errors = [];
|
|
if (item?.schema_version !== "1.0") errors.push("schema_version");
|
|
if (item?.service !== "resend") errors.push("service");
|
|
if (item?.source !== "human_controlled_real") errors.push("source");
|
|
if (item?.status !== "verified") errors.push("status");
|
|
if (item?.domain_controlled !== true) errors.push("domain_controlled");
|
|
if (spf?.status !== "pass") errors.push("spf");
|
|
if (dkim?.status !== "pass") errors.push("dkim");
|
|
if (freeRules?.status !== "verified") errors.push("free_rules.status");
|
|
if (freeRules?.daily_limit !== RESEND_DAILY_LIMIT) errors.push("free_rules.daily_limit");
|
|
if (freeRules?.monthly_limit !== RESEND_MONTHLY_LIMIT) errors.push("free_rules.monthly_limit");
|
|
if (freeRules?.paid_fallback_enabled !== false) errors.push("free_rules.paid_fallback_enabled");
|
|
return errorList(errors);
|
|
}
|
|
|
|
export function validateDeliverySummary(value) {
|
|
const item = object(value);
|
|
const errors = [];
|
|
if (item?.schema_version !== "1.0") errors.push("schema_version");
|
|
if (item?.service !== "resend") errors.push("service");
|
|
if (item?.source !== "human_controlled_real") errors.push("source");
|
|
if (item?.status !== "verified") errors.push("status");
|
|
if (!Array.isArray(item?.categories) || item.categories.length !== DELIVERY_CATEGORIES.length) {
|
|
errors.push("categories");
|
|
} else {
|
|
const categories = item.categories.map((entry) => entry?.category).sort();
|
|
if (categories.join("|") !== DELIVERY_CATEGORIES.slice().sort().join("|")) errors.push("categories.names");
|
|
for (const entry of item.categories) {
|
|
if (!Number.isInteger(entry?.sent_count) || entry.sent_count !== DELIVERY_SAMPLE_SIZE) errors.push(`${entry?.category ?? "unknown"}.sent_count`);
|
|
if (!Number.isInteger(entry?.delivered_within_120_seconds)
|
|
|| entry.delivered_within_120_seconds < DELIVERY_MINIMUM_WITHIN_TWO_MINUTES
|
|
|| entry.delivered_within_120_seconds > DELIVERY_SAMPLE_SIZE) {
|
|
errors.push(`${entry?.category ?? "unknown"}.delivered_within_120_seconds`);
|
|
}
|
|
if (!Number.isFinite(entry?.max_latency_seconds) || entry.max_latency_seconds > DELIVERY_WINDOW_SECONDS) errors.push(`${entry?.category ?? "unknown"}.max_latency_seconds`);
|
|
if (entry?.mock_used !== false) errors.push(`${entry?.category ?? "unknown"}.mock_used`);
|
|
if (entry?.preseeded_account_used !== false) errors.push(`${entry?.category ?? "unknown"}.preseeded_account_used`);
|
|
}
|
|
}
|
|
return errorList(errors);
|
|
}
|
|
|
|
export function validateAuthResult(value) {
|
|
const item = object(value);
|
|
const ordinary = object(item?.ordinary);
|
|
const admin = object(item?.admin);
|
|
const errors = [];
|
|
if (item?.schema_version !== "1.0") errors.push("schema_version");
|
|
if (item?.service !== "resend") errors.push("service");
|
|
if (item?.source !== "human_controlled_real") errors.push("source");
|
|
if (item?.status !== "verified") errors.push("status");
|
|
if (item?.mock_used !== false) errors.push("mock_used");
|
|
if (item?.preseeded_account_used !== false) errors.push("preseeded_account_used");
|
|
for (const [name, auth] of [["ordinary", ordinary], ["admin", admin]]) {
|
|
if (auth?.status !== "passed") errors.push(`${name}.status`);
|
|
if (auth?.chain !== "formal") errors.push(`${name}.chain`);
|
|
if (auth?.verification_code_source !== "real_delivery") errors.push(`${name}.verification_code_source`);
|
|
}
|
|
return errorList(errors);
|
|
}
|
|
|
|
export function validateRedaction(value, serializedEvidence = "") {
|
|
const item = object(value);
|
|
const errors = [];
|
|
if (item?.schema_version !== "1.0") errors.push("schema_version");
|
|
if (item?.status !== "passed") errors.push("status");
|
|
if (item?.forbidden_matches !== 0) errors.push("forbidden_matches");
|
|
if (item?.credentials_in_evidence !== false) errors.push("credentials_in_evidence");
|
|
if (item?.mailboxes_in_evidence !== false) errors.push("mailboxes_in_evidence");
|
|
if (item?.private_content_in_evidence !== false) errors.push("private_content_in_evidence");
|
|
if (item?.absolute_paths_in_evidence !== false) errors.push("absolute_paths_in_evidence");
|
|
if (emailPattern.test(serializedEvidence)) errors.push("email_value");
|
|
if (absolutePathPattern.test(serializedEvidence)) errors.push("absolute_path");
|
|
if (sensitiveKeyPattern.test(serializedEvidence)) errors.push("sensitive_value");
|
|
return errorList(errors);
|
|
}
|
|
|
|
export function validateCandidateReference(record) {
|
|
try {
|
|
validateReleaseCandidateRecord(record);
|
|
} catch (error) {
|
|
return [error instanceof Error ? "candidate_record_invalid" : "candidate_record_invalid"];
|
|
}
|
|
const errors = [];
|
|
if (record.build_commit !== EXPECTED_WP7_01_COMMIT) errors.push("candidate_build_commit");
|
|
const versions = new Map((record.browsers ?? []).map((browser) => [browser.brand, browser.full_version]));
|
|
if (versions.get("Google Chrome") !== "150.0.7871.187") errors.push("chrome_full_version");
|
|
if (versions.get("Microsoft Edge") !== "151.0.4129.59") errors.push("edge_full_version");
|
|
return errorList(errors);
|
|
}
|
|
|
|
export function readJson(path) {
|
|
if (!path || !existsSync(path)) return undefined;
|
|
try {
|
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function fileSha256(path) {
|
|
return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
|
|
}
|
|
|
|
export function validateResendEvidence({ candidate, domainCheck, deliverySummary, authResult, redaction }) {
|
|
const serializedEvidence = JSON.stringify({ domainCheck, deliverySummary, authResult });
|
|
const errors = [
|
|
...validateCandidateReference(candidate),
|
|
...validateDomainCheck(domainCheck),
|
|
...validateDeliverySummary(deliverySummary),
|
|
...validateAuthResult(authResult),
|
|
...validateRedaction(redaction, serializedEvidence),
|
|
];
|
|
return errorList(errors);
|
|
}
|