# Changelog All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added - **Copy the whole email from the message toolbar.** Ctrl+A cannot select the subject, sender and body at once: the body is rendered in a sandboxed iframe without `allow-same-origin`, so it lives in its own opaque-origin document, and browser selection is per-document. With focus inside the body Ctrl+A selects only the message text; with focus outside it, the body is unreachable. A new toolbar button assembles subject, sender, recipient, cc, date and the plain-text body (every message when the message is part of a thread) and writes it to the clipboard, with a hidden-textarea fallback for contexts where the async Clipboard API is unavailable. ### Changed - **Both READMEs rewritten as a pitch, not a manual.** The lead is now the product's one sentence — a free mailbox for your agent — followed by what the agent gets, a plain four-step setup, and the cost answer. Protocol and architecture detail (the request map, the MCP surface, migration semantics) moved into a collapsed block, so the page reads as a product rather than a manual. The README no longer advertises the **Deploy to Cloudflare** button; `npm run setup` is the only path it describes. Three inbound links were repointed: `SUPPORT.md` → the cost section, by its new heading, and `CHANGELOG.md` → the roadmap section, now "What is coming". - **Deploy to Cloudflare can provision D1 and R2.** `wrangler.jsonc` ships the all-zero D1 `database_id` (`00000000-0000-0000-0000-000000000000`) so the Deploy button can create the database and rewrite the id in the clone. `npm run deploy` applies remote migrations before `wrangler deploy` (already-applied migrations are skipped), and every `wrangler d1 migrations apply` script targets the `DB` binding. `npm run setup` treats that placeholder as unset and writes the real id for a database that already exists under the same name. `package.json` `cloudflare.products` lists Workers, D1, R2, and Workers AI. - **New agent API keys use the `mbx_` prefix.** Fresh keys are `mbx_` followed by 64 lowercase hex characters (32 random bytes, the same entropy as before). Verification still accepts existing `agk_` + 64 hex keys, so a key already deployed keeps working until it is revoked. Retired `mb_` send keys stay rejected. `GET /tools` advertises `auth.prefix` as `mbx_`, and the 401 hint names the new shape. ### Documentation - **Deploy button.** Both READMEs describe the button as cloning the repo and provisioning R2 and D1. The admin wizard, Email Routing (or Platform Settings + Add Domain), and optional Resend remain after that. `npm run deploy` is documented as applying remote migrations. - **README screenshots.** `demo_app.png` and `demo_domains.png` are replaced with the current three-pane inbox (AI assistant draft, Review & send) and the Settings → Domains list. The image paths in both READMEs are unchanged. ## [0.2.1] - 2026-10-13 ### Added - **Deploy freshness guard for a bare `wrangler deploy`.** A bare `wrangler deploy` follows the Vite plugin's config redirect and uploads whatever already sits in `build/` without building, which silently ships a stale artifact (it has happened once). Since `build.command` is deleted from the hand-written `wrangler.jsonc` by the plugin, the assertion is injected into the *generated* config instead: `scripts/inject-deploy-guard.mjs` runs at the end of every `npm run build` and adds `build.command` to `build/server/wrangler.json`, which runs `scripts/deploy-freshness-guard.mjs`. That guard asserts the build is fresh only for `deploy` / `versions upload` (decided from `WRANGLER_COMMAND`, which wrangler exports to the custom-build process); every other command — including `wrangler types`, used by `npm run typecheck` — passes through untouched. When `build/` is older than the sources the deploy fails with the newest source, the oldest artifact, and the two ways to fix it. Escape hatch: `ALLOW_STALE_DEPLOY=1`. - **The external-agent surfaces now describe themselves to a caller.** The MCP `initialize` result carries `instructions` — the domain, the `list_mailboxes` prerequisite, the destructive tools and the outbound-send warning — and every `tools/list` entry carries `annotations`. `GET /tools` gains a top-level `info` block (name, version, auth scheme). The `folder` parameter of `list_emails`, `search_emails` and `move_email` is now a JSON-Schema `enum` sourced from the `Folders` constants in `shared/folders.ts` — the same six ids `initMailboxFolders` seeds — so a schema cannot advertise a folder that does not exist, and the tool descriptions no longer repeat the set as prose. ### Fixed - **First deploy no longer fails on the Vite redirect.** `scripts/setup.mjs` used to append `--config /wrangler.jsonc` to *every* wrangler call, including `deploy`. That bypasses the redirect the Cloudflare Vite plugin writes after each build (`.wrangler/deploy/config.json` → `build/server/wrangler.json`), so wrangler re-bundled the raw `workers/app.ts` on the spot and died on the build-time virtual module `virtual:react-router/server-build`. The `deploy` call (and its `--dry-run` rehearsal) now omits `--config` and follows the redirect instead. - **Every 401 now says how to authenticate.** Both auth surfaces answered with a bare `401 { error }` and no `WWW-Authenticate`, which RFC 7235/9110 make mandatory and which left an agent that guessed a guarded path with no way in. The gateway funnels every rejection through one `unauthorized()` helper and answers `WWW-Authenticate: Bearer realm="mailboxes"`, adding `error="invalid_token"` only when credentials were presented but did not verify (RFC 6750 §3.1); the eight cookie-session 401s answer `Session realm="mailboxes"` — deliberately not `Bearer`, which would send a client looking for an OAuth access token. Both carry `Cache-Control: no-store` and a body `hint`. The gateway's `info.docs` pointer is removed: it named `/.well-known/api-catalog`, a route this worker does not register, so the request fell through to the SPA fallback and returned HTML. - **The MCP `instructions` tool count is interpolated, not hardcoded.** `mcpInstructions()` returned a literal "14 tools" while its own doc comment — and the commit that introduced the literal — claimed the count came from `listMcpTools().length`, and the test asserted the same literal, so the drift the assertion existed to catch could never turn it red. The count is interpolated now and the test derives its expectation from `listMcpTools().length`; for the current 14 tools the rendered string is byte-for-byte what it was before the fix. - **Domain verification accepts the real Cloudflare Email Routing MX hosts.** verify-mx compared every exchange against a single hardcoded `mailboxes.pages.dev`, so the four live domains — whose MX is Cloudflare Email Routing's `route*.mx.cloudflare.net` — reported `verified: false` even though inbound was configured correctly. The literal is replaced by `ACCEPTED_INBOUND_MX_TARGETS`, compared for exact membership (case-insensitive, trailing dot stripped); the legacy target stays accepted for compatibility. - **The dashboard's "latest email" line is scoped to the inbox, and says whether it is read.** `getMailboxLatestEmails` grouped by `mailbox_id` alone, so a newer draft, sent or trashed email could hijack the line — `inbox@demo.local` rendered a draft titled `s` instead of the newest received mail. The query now filters `folder_id = 'inbox'`, as `getMailboxUnreadCounts` always has, breaks `date` ties by `id` and never lets an undated row win. That row's `read` flag is exposed as `latest_read`, rendered on the homepage as a brand-coloured icon and a bolded subject, and the row polls every 30s instead of going stale; `latest_snippet` no longer leaks `