Skip to content

fix(core): send Cache-Control private, no-store on the admin shell - #1741

Merged
ascorbic merged 2 commits into
emdash-cms:mainfrom
swissky:fix/admin-shell-no-store
Jul 6, 2026
Merged

ascorbic merged 2 commits into
emdash-cms:mainfrom
swissky:fix/admin-shell-no-store

Conversation

@swissky

@swissky swissky commented Jul 3, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

The admin shell response (/_emdash/admin/[...path], admin.astro) is served without any Cache-Control header. Shared caches that apply RFC 9111 heuristic freshness will store it — for example, Cloudflare's new Workers Cache ("cache": { "enabled": true }) caches header-less 200s for 2 hours. In that setup an authenticated editor's 200 shell can be replayed to anonymous visitors instead of the login redirect, and since the shell is personalized per request (locale via cookie / Accept-Language), the cached variant can also be wrong for the requester.

The API layer already guards against this (API_CACHE_HEADERS sends private, no-store on every API response); this closes the same gap for the admin HTML by setting Cache-Control: private, no-store on the shell response.

Also adds a short "Workers Cache" section to the Cloudflare deployment docs describing how EmDash behaves with the feature enabled and what to watch out for (heuristic freshness on header-less custom routes, visual-editing toolbar on cached public pages).

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Cursor + Claude

Screenshots / test output

Added an e2e regression test (e2e/tests/auth.spec.ts) that requests /_emdash/admin/login and asserts the cache-control: private, no-store response header.

Verified on a deployed Cloudflare Worker with Workers Cache enabled: before the change GET /_emdash/admin/login returned 200 with no Cache-Control; after the change it returns cache-control: private, no-store and Workers Cache reports BYPASS instead of storing the shell.

pnpm --filter emdash typecheck and the core test suite pass locally (4269 passed; the 8 failures in tests/unit/astro/vite-config.test.ts are environment-only — they require a built @emdash-cms/admin/dist, whose build currently fails on Node 24.1.0 with ERR_INTERNAL_ASSERTION in lingui, unrelated to this change).

@changeset-bot

changeset-bot Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b26cd8f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1741

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1741

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1741

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1741

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1741

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1741

emdash

npm i https://pkg.pr.new/emdash@1741

create-emdash

npm i https://pkg.pr.new/create-emdash@1741

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1741

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1741

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1741

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1741

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1741

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1741

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1741

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1741

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1741

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1741

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1741

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1741

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1741

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1741

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1741

commit: b26cd8f

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right change, implemented in the right place. The admin shell (/_emdash/admin/[...path]) is served by a single catch-all Astro route, and because it is personalized per request (locale) and drives authenticated flows, it must not be stored by shared caches. Setting Cache-Control: private, no-store in packages/core/src/astro/routes/admin.astro mirrors the existing API_CACHE_HEADERS pattern in api/error.ts and the middleware cache headers in astro/middleware/auth.ts. The header survives the finalizeResponse wrapper because that helper only sets-if-absent baseline security headers and does not touch Cache-Control.

The Cloudflare docs addition is accurate and useful, and the changeset is a user-facing patch note for the emdash package.

The only gap is the missing regression test. AGENTS.md emphasizes TDD for bugs, and this PR is explicitly categorized as a bug fix. The author acknowledged this by leaving the test checklist item unchecked, but a regression test—likely an integration or e2e test that hits /_emdash/admin/login and asserts the response header—would prevent future regressions if the shell is refactored. Since the code itself is correct and verified manually, I’m treating this as a non-blocking suggestion rather than a request for changes.

// of the login redirect. The shell is also personalized per request (locale
// via cookie/Accept-Language). API routes already send `private, no-store`
// (API_CACHE_HEADERS); this closes the same gap for the admin HTML.
Astro.response.headers.set("Cache-Control", "private, no-store");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The new Cache-Control: private, no-store header is correct, but this is a bug fix with no reproducing test. AGENTS.md requires TDD for bugs, and the author left the test checklist item unchecked. Consider adding an integration or e2e test that requests /_emdash/admin/login (or any admin-shell path) and asserts the response header is private, no-store, similar to the existing request-context-toolbar-cache.test.ts and cache-headers.test.ts regression tests.

@github-actions github-actions Bot added review/needs-rereview New commits since the last review and removed review/needs-review No maintainer or bot review yet labels Jul 3, 2026
@swissky

swissky commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Fair point on the missing regression test — added one in b26cd8f: an e2e test in e2e/tests/auth.spec.ts that requests /_emdash/admin/login and asserts cache-control: private, no-store on the response. It exercises the real admin-shell route through the middleware stack, so it also covers the finalizeResponse interaction and will catch a regression if the shell is refactored.

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@ascorbic
ascorbic merged commit 1866fa3 into emdash-cms:main Jul 6, 2026
51 checks passed
@emdashbot emdashbot Bot mentioned this pull request Jul 6, 2026
@swissky
swissky deleted the fix/admin-shell-no-store branch September 10, 2026 06:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants