Minor Changes
-
#705
8ebdf1aThanks @eba8! - Adds admin white-labeling support viaadminconfig inastro.config.mjs. Agencies can set a custom logo, site name, and favicon for the admin panel, separate from public site settings. -
#742
c26442bThanks @ascorbic! - AddstrustedProxyHeadersconfig option so self-hosted deployments behind a reverse proxy can declare which client-IP headers to trust. Used by auth rate limits (magic-link, signup, passkey, OAuth device flow) and the public comment endpoint — without it, every request on a non-Cloudflare deployment was treated as "unknown" and rate limits were effectively disabled.Set the option in
astro.config.mjs:emdash({ trustedProxyHeaders: ["x-real-ip"], // nginx, Caddy, Traefik });
or via the
EMDASH_TRUSTED_PROXY_HEADERSenv var (comma-separated). Headers are tried in order; values ending inforwarded-forare parsed as comma-separated lists.Also removes the user-agent-hash fallback on the comment endpoint. The fallback was meant to give anonymous commenters on non-Cloudflare deployments something approximating per-user rate limiting, but the UA is trivially rotatable; requests with no trusted IP now share a stricter "unknown" bucket. Operators behind a reverse proxy should set
trustedProxyHeadersto restore per-IP bucketing.Only set
trustedProxyHeaderswhen you control the reverse proxy. Trusting a forwarded-IP header from the open internet lets any client spoof their IP and defeats rate limiting.
Patch Changes
-
#745
7186961Thanks @ascorbic! - Fixes an unauthenticated denial-of-service via the 404 log. Every 404 response previously inserted a new row into_emdash_404_log, so an attacker could grow the database without bound by requesting unique nonexistent URLs. Repeat hits to the same path now dedup into a single row with ahitscounter andlast_seen_attimestamp, referrer and user-agent headers are truncated to bounded lengths, and the log is capped at 10,000 rows with oldest-first eviction. -
#739
e9ecec2Thanks @MohamedH1998! - Fixes the REST content API silently strippingpublishedAton create/update andcreatedAton create. Importers can now preserve original publish and creation dates on migrated content. Gated behindcontent:publish_any(EDITOR+) so regular contributors cannot backdate posts.createdAtis intentionally not accepted on update —created_atis treated as immutable. -
#732
e3e18aaThanks @jcheese1! - Fixes select dropdown appearing behind dialog by removing explicit z-index values and addingisolateto the admin body for proper stacking context. -
#695
fae63bdThanks @ascorbic! - Fixesemdash seedso entries declared with"status": "published"are actually published. Previously the seed wrote the content row withstatus: "published"and apublished_attimestamp but never created a live revision, so the admin UI showed "Save & Publish" instead of "Unpublish" andlive_revision_idstayed null. The seed now promotes published entries to a live revision on both create and update paths. -
#744
30d8fe0Thanks @ascorbic! - Fixes a setup-window admin hijack by binding/setup/adminand/setup/admin/verifyto a per-session nonce cookie. Previously an unauthenticated attacker who could reach a site during first-time setup could POST to/setup/adminbetween the legitimate admin's email submission and passkey verification, overwriting the stored email — the admin account would then be created with the attacker's address. The admin route now mints a cryptographically random nonce, stores it in setup state, and sets it as an HttpOnly, SameSite=Strict,/_emdash/-scoped cookie; the verify route rejects any request whose cookie does not match in constant time. -
#685
d4a95bfThanks @ascorbic! - Fixes visual editing: clicking an editable field now opens the inline editor instead of always opening the admin in a new tab. The toolbar's manifest fetch was readingmanifest.collectionsdirectly but the/_emdash/api/manifestendpoint wraps its payload in{ data: … }, so every field-kind lookup returnednulland every click fell through to the admin-new-tab fallback. -
#743
a31db7dThanks @ascorbic! - Locksemdash:site_urlafter the first setup call so a spoofed Host header on a later step of the wizard can't overwrite it. Config (siteUrl) and env (EMDASH_SITE_URL) paths already took precedence; this is a defence-in-depth guard for deployments that rely on the request-origin fallback. -
#737
adb118cThanks @ascorbic! - Rate-limits the self-signup request endpoint to prevent abuse.POST /_emdash/api/auth/signup/requestnow allows 3 requests per 5 minutes per IP, matching the existing limit on magic-link/send. Over-limit requests return the same generic success response as allowed-but-ignored requests, so the limit isn't observable to callers. -
#738
080a4f1Thanks @ascorbic! - Strengthens SSRF protection on the import pipeline against DNS-rebinding. ThevalidateExternalUrlhelper now also blocks known wildcard DNS services (nip.io,sslip.io,xip.io,traefik.me,lvh.me,localtest.me) and trailing-dot FQDN forms of blocked hostnames. A newresolveAndValidateExternalUrlresolves the target hostname via DNS-over-HTTPS (Cloudflare) and rejects if any returned IP is in a private range.ssrfSafeFetchand the plugin unrestricted-fetch path now use the DNS-aware validator on every hop. This adds two DoH round-trips per outbound request; self-hosted admins whose egress blockscloudflare-dns.comcan inject a custom resolver viasetDefaultDnsResolver. -
#736
81fe93bThanks @ascorbic! - Restricts Subscriber-role access to draft, scheduled, and trashed content. Subscribers retaincontent:readfor member-only published content but no longer see non-published items via the REST API or MCP server. Adds a newcontent:read_draftspermission (Contributor and above) that gates/compare,/revisions,/trash,/preview-url, and the corresponding MCP tools. -
Updated dependencies [
8ebdf1a,2e4b205,e3e18aa,743b080,fa8d753,81fe93b]:- @emdash-cms/admin@0.7.0
- @emdash-cms/auth@0.7.0
- @emdash-cms/gutenberg-to-portable-text@0.7.0