agents
Version:
A home for your AI agents
223 lines (222 loc) • 8.72 kB
JavaScript
//#region src/email.ts
/**
* Check if an email appears to be an auto-reply based on standard headers.
* Checks for Auto-Submitted (RFC 3834), X-Auto-Response-Suppress, and Precedence headers.
*
* @param headers - Headers array from postal-mime Email.headers or similar format
* @returns true if email appears to be an auto-reply
*
* @example
* ```typescript
* if (isAutoReplyEmail(parsed.headers)) {
* // Skip processing auto-replies
* return;
* }
* ```
*/
function isAutoReplyEmail(headers) {
return headers.some((h) => {
const key = h.key.toLowerCase();
const value = h.value.toLowerCase();
if (key === "auto-submitted") return value !== "no";
if (key === "x-auto-response-suppress") return true;
if (key === "precedence") return value === "bulk" || value === "junk" || value === "list";
return false;
});
}
/** Default signature expiration: 30 days in seconds */
const DEFAULT_MAX_AGE_SECONDS = 720 * 60 * 60;
/** Maximum allowed clock skew for future timestamps: 5 minutes */
const MAX_CLOCK_SKEW_SECONDS = 300;
/**
* Compute HMAC-SHA256 signature for agent routing headers
* @param secret - Secret key for HMAC
* @param agentName - Name of the agent
* @param agentId - ID of the agent instance
* @param timestamp - Unix timestamp in seconds
* @returns Base64-encoded HMAC signature
*/
async function computeAgentSignature(secret, agentName, agentId, timestamp) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
name: "HMAC",
hash: "SHA-256"
}, false, ["sign"]);
const data = encoder.encode(`${agentName}:${agentId}:${timestamp}`);
const signature = await crypto.subtle.sign("HMAC", key, data);
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}
/**
* Verify HMAC-SHA256 signature for agent routing headers
* @param secret - Secret key for HMAC
* @param agentName - Name of the agent
* @param agentId - ID of the agent instance
* @param signature - Base64-encoded signature to verify
* @param timestamp - Unix timestamp in seconds when signature was created
* @param maxAgeSeconds - Maximum age of signature in seconds (default: 30 days)
* @returns Verification result with reason if invalid
*/
async function verifyAgentSignature(secret, agentName, agentId, signature, timestamp, maxAgeSeconds = DEFAULT_MAX_AGE_SECONDS) {
try {
const timestampNum = Number.parseInt(timestamp, 10);
if (Number.isNaN(timestampNum)) return {
valid: false,
reason: "malformed_timestamp"
};
const now = Math.floor(Date.now() / 1e3);
if (timestampNum > now + MAX_CLOCK_SKEW_SECONDS) return {
valid: false,
reason: "invalid"
};
if (now - timestampNum > maxAgeSeconds) return {
valid: false,
reason: "expired"
};
const expected = await computeAgentSignature(secret, agentName, agentId, timestamp);
if (expected.length !== signature.length) return {
valid: false,
reason: "invalid"
};
let result = 0;
for (let i = 0; i < expected.length; i++) result |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
if (result !== 0) return {
valid: false,
reason: "invalid"
};
return { valid: true };
} catch (error) {
console.warn("[agents] Signature verification error:", error);
return {
valid: false,
reason: "invalid"
};
}
}
/**
* Sign agent routing headers for secure reply flows.
* Use this when sending outbound emails to ensure replies can be securely routed back.
*
* @param secret - Secret key for HMAC signing (store in environment variables)
* @param agentName - Name of the agent
* @param agentId - ID of the agent instance
* @returns Headers object with X-Agent-Name, X-Agent-ID, X-Agent-Sig, and X-Agent-Sig-Ts
*
* @example
* ```typescript
* const headers = await signAgentHeaders(env.EMAIL_SECRET, "MyAgent", this.name);
* // Use these headers when sending outbound emails
* ```
*/
async function signAgentHeaders(secret, agentName, agentId) {
if (!secret) throw new Error("secret is required for signing agent headers");
if (!agentName) throw new Error("agentName is required for signing agent headers");
if (!agentId) throw new Error("agentId is required for signing agent headers");
if (agentName.includes(":")) throw new Error("agentName cannot contain colons");
if (agentId.includes(":")) throw new Error("agentId cannot contain colons");
const timestamp = Math.floor(Date.now() / 1e3).toString();
const signature = await computeAgentSignature(secret, agentName, agentId, timestamp);
return {
"X-Agent-Name": agentName,
"X-Agent-ID": agentId,
"X-Agent-Sig": signature,
"X-Agent-Sig-Ts": timestamp
};
}
/**
* @deprecated REMOVED due to security vulnerability (IDOR via spoofed headers).
* @throws Always throws an error with migration guidance.
*/
function createHeaderBasedEmailResolver() {
throw new Error("createHeaderBasedEmailResolver has been removed due to a security vulnerability. It trusted attacker-controlled email headers for routing, enabling IDOR attacks.\n\nMigration options:\n - For inbound mail: use createAddressBasedEmailResolver(agentName)\n - For reply flows: use createSecureReplyEmailResolver(secret) with signed headers\n\nSee https://github.com/cloudflare/agents/blob/main/docs/email.md for details.");
}
/**
* Create a resolver for routing email replies with signature verification.
* This resolver verifies that replies contain a valid HMAC signature, preventing
* attackers from routing emails to arbitrary agent instances.
*
* @param secret - Secret key for HMAC verification (must match the key used with signAgentHeaders)
* @param options - Optional configuration for signature verification
* @returns A function that resolves the agent to route the email to, or null if signature is invalid
*
* @example
* ```typescript
* // In your email handler
* const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
* maxAge: 7 * 24 * 60 * 60, // 7 days
* onInvalidSignature: (email, reason) => {
* console.warn(`Invalid signature from ${email.from}: ${reason}`);
* }
* });
* const addressResolver = createAddressBasedEmailResolver("MyAgent");
*
* await routeAgentEmail(email, env, {
* resolver: async (email, env) => {
* // Try secure reply routing first
* const replyRouting = await secureResolver(email, env);
* if (replyRouting) return replyRouting;
* // Fall back to address-based routing
* return addressResolver(email, env);
* }
* });
* ```
*/
function createSecureReplyEmailResolver(secret, options) {
if (!secret) throw new Error("secret is required for createSecureReplyEmailResolver");
const maxAge = options?.maxAge ?? DEFAULT_MAX_AGE_SECONDS;
const onInvalidSignature = options?.onInvalidSignature;
return async (email, _env) => {
const agentName = email.headers.get("x-agent-name");
const agentId = email.headers.get("x-agent-id");
const signature = email.headers.get("x-agent-sig");
const timestamp = email.headers.get("x-agent-sig-ts");
if (!agentName || !agentId || !signature || !timestamp) {
onInvalidSignature?.(email, "missing_headers");
return null;
}
const result = await verifyAgentSignature(secret, agentName, agentId, signature, timestamp, maxAge);
if (!result.valid) {
onInvalidSignature?.(email, result.reason);
return null;
}
return {
agentName,
agentId,
_secureRouted: true
};
};
}
/**
* Create a resolver that uses the email address to determine the agent to route the email to
* @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
* @returns A function that resolves the agent to route the email to
*/
function createAddressBasedEmailResolver(defaultAgentName) {
return async (email, _env) => {
const emailMatch = email.to.match(/^([^+@]{1,64})(?:\+([^@]{1,64}))?@(.{1,253})$/);
if (!emailMatch) return null;
const [, localPart, subAddress] = emailMatch;
if (subAddress) return {
agentName: localPart,
agentId: subAddress
};
return {
agentName: defaultAgentName,
agentId: localPart
};
};
}
/**
* Create a resolver that uses the agentName and agentId to determine the agent to route the email to
* @param agentName The name of the agent to route the email to
* @param agentId The id of the agent to route the email to
* @returns A function that resolves the agent to route the email to
*/
function createCatchAllEmailResolver(agentName, agentId) {
return async () => ({
agentName,
agentId
});
}
//#endregion
export { createSecureReplyEmailResolver as a, createHeaderBasedEmailResolver as i, createAddressBasedEmailResolver as n, isAutoReplyEmail as o, createCatchAllEmailResolver as r, signAgentHeaders as s, DEFAULT_MAX_AGE_SECONDS as t };
//# sourceMappingURL=email-XHsSYsTO.js.map