branch:
SecureDemo.tsx
18880 bytesRaw
import { useAgent } from "agents/react";
import { useState } from "react";
import {
  ShieldIcon,
  PaperPlaneTiltIcon,
  TrayIcon,
  LockIcon,
  CheckCircleIcon
} from "@phosphor-icons/react";
import {
  Button,
  Surface,
  Badge,
  Switch,
  Tabs,
  Empty,
  Text
} from "@cloudflare/kumo";
import { DemoWrapper } from "../../layout";
import {
  LogPanel,
  ConnectionStatus,
  LocalDevBanner,
  CodeExplanation,
  type CodeSection
} from "../../components";
import { useLogs, useUserId } from "../../hooks";
import type {
  SecureEmailAgent,
  SecureEmailState,
  ParsedEmail,
  SentReply
} from "./secure-email-agent";

type TabType = "inbox" | "outbox";

const codeSections: CodeSection[] = [
  {
    title: "Send signed replies with replyToEmail",
    description:
      "Use the built-in replyToEmail method to send HMAC-signed replies. The SDK attaches X-Agent-Name, X-Agent-ID, X-Agent-Sig, and X-Agent-Sig-Ts headers automatically. When the recipient replies, the signature is verified and routed back to the same agent instance.",
    code: `import { Agent } from "agents";
import type { AgentEmail } from "agents/email";
import { isAutoReplyEmail } from "agents/email";
import PostalMime from "postal-mime";

class SecureEmailAgent extends Agent<Env> {
  async onEmail(email: AgentEmail) {
    const raw = await email.getRaw();
    const parsed = await PostalMime.parse(raw);

    // email._secureRouted is true when the SDK
    // verified the HMAC signature on the reply
    if (email._secureRouted) {
      console.log("Verified reply from:", email.from);
    }

    // Avoid infinite auto-reply loops
    if (!isAutoReplyEmail(parsed.headers)) {
      await this.replyToEmail(email, {
        fromName: "Secure Agent",
        body: "Thanks for your message!",
        secret: this.env.EMAIL_SECRET,
      });
    }
  }
}`
  },
  {
    title: "Route with secure reply verification",
    description:
      "Combine createSecureReplyEmailResolver with createAddressBasedEmailResolver in routeAgentEmail. Secure replies are checked first — if the HMAC signature is valid, the email routes directly to the originating agent instance. Otherwise, address-based routing takes over.",
    code: `import { routeAgentEmail } from "agents";
import {
  createSecureReplyEmailResolver,
  createAddressBasedEmailResolver,
} from "agents/email";

export default {
  async email(message: ForwardableEmailMessage, env: Env) {
    const secureResolver = createSecureReplyEmailResolver(
      env.EMAIL_SECRET
    );
    const addressResolver = createAddressBasedEmailResolver(
      "SecureEmailAgent"
    );

    await routeAgentEmail(message, env, {
      resolver: async (email, env) => {
        // Signed replies get priority
        const reply = await secureResolver(email, env);
        if (reply) return reply;
        // Fall back to address-based routing
        return addressResolver(email, env);
      },
    });
  },
};`
  }
];

export function SecureDemo() {
  const userId = useUserId();
  const { logs, addLog, clearLogs } = useLogs();
  const [activeTab, setActiveTab] = useState<TabType>("inbox");
  const [selectedEmail, setSelectedEmail] = useState<ParsedEmail | null>(null);
  const [selectedReply, setSelectedReply] = useState<SentReply | null>(null);

  const [state, setState] = useState<SecureEmailState>({
    inbox: [],
    outbox: [],
    totalReceived: 0,
    totalReplies: 0,
    autoReplyEnabled: true
  });

  const agent = useAgent<SecureEmailAgent, SecureEmailState>({
    agent: "secure-email-agent",
    name: `email-secure-${userId}`,
    onStateUpdate: (newState) => {
      if (newState) {
        setState(newState);
        addLog("in", "state_update", {
          inbox: newState.inbox.length,
          outbox: newState.outbox.length
        });
      }
    },
    onOpen: () => addLog("info", "connected"),
    onClose: () => addLog("info", "disconnected"),
    onError: () => addLog("error", "error", "Connection error"),
    onMessage: (message) => {
      try {
        const data = JSON.parse(message.data as string);
        if (data.type) {
          addLog("in", data.type, data);
        }
      } catch {
        // ignore
      }
    }
  });

  const handleToggleAutoReply = async () => {
    addLog("out", "toggleAutoReply");
    try {
      await agent.call("toggleAutoReply");
    } catch (e) {
      addLog("error", "error", e instanceof Error ? e.message : String(e));
    }
  };

  const handleClearEmails = async () => {
    addLog("out", "clearEmails");
    try {
      await agent.call("clearEmails");
      setSelectedEmail(null);
      setSelectedReply(null);
    } catch (e) {
      addLog("error", "error", e instanceof Error ? e.message : String(e));
    }
  };

  return (
    <DemoWrapper
      title="Secure Email Replies"
      description={
        <>
          When replying to emails, agents can include HMAC-signed headers that
          identify the originating instance. When the recipient replies back,
          the signature is verified and the email routes to the correct agent
          automatically. Tokens use the{" "}
          <code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
            EMAIL_SECRET
          </code>{" "}
          environment variable for signing.
        </>
      }
      statusIndicator={
        <ConnectionStatus
          status={
            agent.readyState === WebSocket.OPEN ? "connected" : "connecting"
          }
        />
      }
    >
      <LocalDevBanner />

      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-4">
        {/* Left Panel - Info & Settings */}
        <div className="space-y-6">
          <Surface className="p-4 rounded-lg ring ring-kumo-line">
            <div className="text-xs text-kumo-subtle">
              Instance:{" "}
              <code className="bg-kumo-control px-1 rounded text-kumo-default">
                demo
              </code>
            </div>
          </Surface>

          <Surface className="p-4 rounded-lg ring ring-kumo-line">
            <div className="mb-4">
              <Text variant="heading3">Stats</Text>
            </div>
            <div className="grid grid-cols-2 gap-3">
              <div className="p-3 bg-kumo-elevated rounded">
                <div className="flex items-center gap-2 text-kumo-subtle text-xs mb-1">
                  <TrayIcon size={12} />
                  Received
                </div>
                <div className="text-2xl font-semibold text-kumo-default">
                  {state.totalReceived}
                </div>
              </div>
              <div className="p-3 bg-kumo-elevated rounded">
                <div className="flex items-center gap-2 text-kumo-subtle text-xs mb-1">
                  <PaperPlaneTiltIcon size={12} />
                  Replies
                </div>
                <div className="text-2xl font-semibold text-kumo-default">
                  {state.totalReplies}
                </div>
              </div>
            </div>
          </Surface>

          <Surface className="p-4 rounded-lg ring ring-kumo-line">
            <div className="mb-3">
              <Text variant="heading3">Settings</Text>
            </div>
            <Switch
              label="Auto-reply with signed headers"
              checked={state.autoReplyEnabled}
              onCheckedChange={handleToggleAutoReply}
            />
            <p className="text-xs text-kumo-subtle mt-2">
              When enabled, incoming emails receive a signed reply that can be
              securely routed back.
            </p>
          </Surface>

          <Surface className="p-4 rounded-lg bg-kumo-elevated">
            <div className="flex items-center gap-2 mb-3">
              <ShieldIcon size={16} />
              <Text variant="heading3">How Secure Replies Work</Text>
            </div>
            <ol className="text-sm text-kumo-subtle space-y-2">
              <li>
                <strong className="text-kumo-default">1.</strong> Email arrives
                at{" "}
                <code className="text-xs bg-kumo-control px-1 rounded text-kumo-default">
                  secure+demo@domain
                </code>
              </li>
              <li>
                <strong className="text-kumo-default">2.</strong> Agent sends
                reply with signed headers:
                <ul className="mt-1 ml-4 text-xs space-y-0.5">
                  <li>
                    <code className="text-kumo-default">X-Agent-Name</code>
                  </li>
                  <li>
                    <code className="text-kumo-default">X-Agent-ID</code>
                  </li>
                  <li>
                    <code className="text-kumo-default">X-Agent-Sig</code>{" "}
                    (HMAC)
                  </li>
                  <li>
                    <code className="text-kumo-default">X-Agent-Sig-Ts</code>
                  </li>
                </ul>
              </li>
              <li>
                <strong className="text-kumo-default">3.</strong> When user
                replies, signature is verified
              </li>
              <li>
                <strong className="text-kumo-default">4.</strong> Valid replies
                route back to same agent instance
              </li>
            </ol>
          </Surface>

          <Surface className="p-4 rounded-lg ring ring-kumo-line">
            <div className="mb-2">
              <Text bold size="sm">
                Production Setup
              </Text>
            </div>
            <div className="text-xs text-kumo-subtle space-y-1">
              <div>Set a secure secret:</div>
              <code className="block bg-kumo-control px-2 py-1 rounded mt-1 text-kumo-default">
                wrangler secret put EMAIL_SECRET
              </code>
            </div>
          </Surface>
        </div>

        {/* Center Panel - Mailboxes */}
        <div className="space-y-6">
          <Surface className="overflow-hidden rounded-lg ring ring-kumo-line">
            {/* Tabs */}
            <Tabs
              variant="segmented"
              value={activeTab}
              onValueChange={(value) => {
                setActiveTab(value as TabType);
                setSelectedEmail(null);
                setSelectedReply(null);
              }}
              tabs={[
                {
                  value: "inbox",
                  label: (
                    <span className="flex items-center gap-2">
                      <TrayIcon size={16} /> Inbox ({state.inbox.length})
                    </span>
                  )
                },
                {
                  value: "outbox",
                  label: (
                    <span className="flex items-center gap-2">
                      <PaperPlaneTiltIcon size={16} /> Outbox (
                      {state.outbox.length})
                    </span>
                  )
                }
              ]}
              className="m-2"
            />

            {/* Email List */}
            <div className="max-h-64 overflow-y-auto">
              {activeTab === "inbox" ? (
                state.inbox.length > 0 ? (
                  [...state.inbox].reverse().map((email) => (
                    <button
                      key={email.id}
                      type="button"
                      onClick={() => {
                        setSelectedEmail(email);
                        setSelectedReply(null);
                      }}
                      className={`w-full text-left p-3 border-b border-kumo-fill last:border-0 hover:bg-kumo-tint transition-colors ${
                        selectedEmail?.id === email.id ? "bg-kumo-control" : ""
                      }`}
                    >
                      <div className="flex items-center justify-between mb-1">
                        <div className="flex items-center gap-2">
                          {email.isSecureReply && (
                            <LockIcon size={12} className="text-kumo-success" />
                          )}
                          <span className="text-sm font-medium truncate text-kumo-default">
                            {email.from}
                          </span>
                        </div>
                        <span className="text-xs text-kumo-inactive">
                          {new Date(email.timestamp).toLocaleTimeString()}
                        </span>
                      </div>
                      <p className="text-sm text-kumo-subtle truncate">
                        {email.subject}
                      </p>
                    </button>
                  ))
                ) : (
                  <div className="py-8">
                    <Empty title="No emails received" size="sm" />
                  </div>
                )
              ) : state.outbox.length > 0 ? (
                [...state.outbox].reverse().map((reply) => (
                  <button
                    key={reply.id}
                    type="button"
                    onClick={() => {
                      setSelectedReply(reply);
                      setSelectedEmail(null);
                    }}
                    className={`w-full text-left p-3 border-b border-kumo-fill last:border-0 hover:bg-kumo-tint transition-colors ${
                      selectedReply?.id === reply.id ? "bg-kumo-control" : ""
                    }`}
                  >
                    <div className="flex items-center justify-between mb-1">
                      <div className="flex items-center gap-2">
                        {reply.signed && (
                          <CheckCircleIcon
                            size={12}
                            className="text-kumo-success"
                          />
                        )}
                        <span className="text-sm font-medium truncate text-kumo-default">
                          {reply.to}
                        </span>
                      </div>
                      <span className="text-xs text-kumo-inactive">
                        {new Date(reply.timestamp).toLocaleTimeString()}
                      </span>
                    </div>
                    <p className="text-sm text-kumo-subtle truncate">
                      {reply.subject}
                    </p>
                  </button>
                ))
              ) : (
                <div className="py-8">
                  <Empty title="No replies sent" size="sm" />
                </div>
              )}
            </div>

            {/* Clear button */}
            {(state.inbox.length > 0 || state.outbox.length > 0) && (
              <div className="p-2 border-t border-kumo-line">
                <Button
                  variant="ghost"
                  size="xs"
                  onClick={handleClearEmails}
                  className="text-kumo-danger"
                >
                  Clear all emails
                </Button>
              </div>
            )}
          </Surface>

          {/* Email Detail */}
          {selectedEmail && (
            <Surface className="p-4 rounded-lg ring ring-kumo-line">
              <div className="mb-3">
                <div className="flex items-center justify-between">
                  <div className="flex items-center gap-2">
                    {selectedEmail.isSecureReply && (
                      <Badge variant="primary">
                        <span className="flex items-center gap-1">
                          <LockIcon size={12} />
                          Secure Reply
                        </span>
                      </Badge>
                    )}
                    <Text variant="heading3">{selectedEmail.subject}</Text>
                  </div>
                  <Button
                    variant="ghost"
                    shape="square"
                    size="xs"
                    aria-label="Close email"
                    onClick={() => setSelectedEmail(null)}
                  >
                    ×
                  </Button>
                </div>
                <div className="text-xs text-kumo-subtle mt-1">
                  <div>From: {selectedEmail.from}</div>
                  <div>To: {selectedEmail.to}</div>
                  <div>
                    Date: {new Date(selectedEmail.timestamp).toLocaleString()}
                  </div>
                </div>
              </div>
              <div className="bg-kumo-recessed rounded p-3 text-sm whitespace-pre-wrap max-h-48 overflow-y-auto text-kumo-default">
                {selectedEmail.text || selectedEmail.html || "(No content)"}
              </div>
            </Surface>
          )}

          {/* Reply Detail */}
          {selectedReply && (
            <Surface className="p-4 rounded-lg ring ring-kumo-line">
              <div className="mb-3">
                <div className="flex items-center justify-between">
                  <div className="flex items-center gap-2">
                    {selectedReply.signed && (
                      <Badge variant="primary">
                        <span className="flex items-center gap-1">
                          <CheckCircleIcon size={12} />
                          Signed
                        </span>
                      </Badge>
                    )}
                    <Text variant="heading3">{selectedReply.subject}</Text>
                  </div>
                  <Button
                    variant="ghost"
                    shape="square"
                    size="xs"
                    aria-label="Close reply"
                    onClick={() => setSelectedReply(null)}
                  >
                    ×
                  </Button>
                </div>
                <div className="text-xs text-kumo-subtle mt-1">
                  <div>To: {selectedReply.to}</div>
                  <div>
                    Date: {new Date(selectedReply.timestamp).toLocaleString()}
                  </div>
                </div>
              </div>
              <div className="bg-kumo-recessed rounded p-3 text-sm whitespace-pre-wrap max-h-48 overflow-y-auto text-kumo-default">
                {selectedReply.body}
              </div>
              {selectedReply.signed && (
                <div className="mt-3 p-2 bg-green-500/10 rounded text-xs text-kumo-success">
                  This reply includes signed X-Agent-* headers for secure
                  routing.
                </div>
              )}
            </Surface>
          )}
        </div>

        {/* Right Panel - Logs */}
        <div className="space-y-6">
          <LogPanel logs={logs} onClear={clearLogs} maxHeight="500px" />
        </div>
      </div>

      <CodeExplanation sections={codeSections} />
    </DemoWrapper>
  );
}