UNPKG

agents

Version:

A home for your AI agents

222 lines (221 loc) 8.63 kB
//#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(); return { "X-Agent-Name": agentName, "X-Agent-ID": agentId, "X-Agent-Sig": await computeAgentSignature(secret, agentName, agentId, timestamp), "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/agents/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 ?? 2592e3; 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 { DEFAULT_MAX_AGE_SECONDS, createAddressBasedEmailResolver, createCatchAllEmailResolver, createHeaderBasedEmailResolver, createSecureReplyEmailResolver, isAutoReplyEmail, signAgentHeaders }; //# sourceMappingURL=email.js.map