Skip to content

feat: cache purge API (object cache + native Workers Caching) - #2275

Merged
scottbuscemi merged 7 commits into
mainfrom
feat/admin-cache-purge
Jul 29, 2026
Merged

feat: cache purge API (object cache + native Workers Caching)#2275
scottbuscemi merged 7 commits into
mainfrom
feat/admin-cache-purge

Conversation

@scottbuscemi

@scottbuscemi scottbuscemi commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds a first-party way for admins and sandboxed plugins to clear EmDash caches.

CMS object cache (KV / memory)

  • Admin API: GET / POST /_emdash/api/admin/cache/object (settings:manage)
    • GET reports whether an object-cache backend is configured
    • POST bumps epochs for fixed namespaces + content collections (or a selected subset)
  • Plugin: ctx.cache.getObjectCacheStatus() / ctx.cache.purgeObjectCache()

Workers Caching (native edge page cache)

  • Admin API: GET / POST /_emdash/api/admin/cache/workers (settings:manage)
    • GET reports whether native cache.purge is available
    • POST calls cache.purge({ purgeEverything: true }) or cache.purge({ pathPrefixes })
  • Plugin: ctx.cache.getWorkersCacheStatus() / ctx.cache.purgeWorkersCache({ pathPrefixes? })
  • Uses platform cache.purge via virtual:emdash/workers-cache (no zone ID / API token)
  • Status configured: true when cache.purge is a function (production Workers); local workerd may expose cache without purge

Shared

  • Capability cache:purge gates both in-process and through Cloudflare / workerd sandbox bridges
  • Block Kit buttons support optional disabled and title (tooltip)

Closes #

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: n/a (EmDash maintainer)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: opencode + xAI grok-4.5

Screenshots / test output

Targeted tests: object-cache + workers-cache handlers/routes, virtual module generator, marketplace CAPABILITY_LABELS, blocks disabled+title.

Preferred site setup (see also #2277):

// wrangler.jsonc
{ "cache": { "enabled": true } }
// astro.config.mjs
import { cacheCloudflare } from "@astrojs/cloudflare/cache";
cache: { provider: cacheCloudflare() },

Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks feat/admin-cache-purge. Updated automatically when the playground redeploys.

Admins and sandboxed plugins can clear CMS object-cache namespaces
(KV/memory) via GET/POST /_emdash/api/admin/cache/object and
ctx.cache. Block Kit buttons gain optional disabled and title fields
for clearer troubleshooting UI.
@changeset-bot

changeset-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5e2fe09

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

This PR includes changesets to release 20 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Minor
@emdash-cms/plugin-types Minor
@emdash-cms/plugin-cli Patch
@emdash-cms/blocks Minor
@emdash-cms/plugin-mcp-smoke Major
@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 Minor
@emdash-cms/registry-verification Patch
@emdash-cms/plugin-embeds Patch
@emdash-cms/auth Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto 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

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 790 lines across 28 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do 5e2fe09 Jul 29 2026, 10:07 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 5e2fe09 Jul 29 2026, 10:01 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 5e2fe09 Jul 29 2026, 10:01 PM

@pkg-pr-new

pkg-pr-new Bot commented Jul 29, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2275

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 13c411a

Admins and plugins with cache:purge can clear edge-cached pages via
GET/POST /_emdash/api/admin/cache/workers and ctx.cache.purgeWorkersCache()
(Cloudflare purge_everything using CF_ZONE_ID + CF_CACHE_PURGE_TOKEN).
@scottbuscemi scottbuscemi changed the title feat: object-cache purge API and cache:purge plugin capability feat: cache purge API (object cache + Workers Cache) and cache:purge plugin capability Jul 29, 2026
Replace zone REST purge (CF_ZONE_ID + token) with cloudflare:workers
cache.purge({ purgeEverything: true }). Status is configured when the
native API is available — no secrets required.
Dynamic import of cloudflare:workers from core failed under Vite.
Expose cache through virtual:emdash/workers-cache (same pattern as env
and waitUntil) so status/purge work on the Cloudflare adapter.
@scottbuscemi scottbuscemi changed the title feat: cache purge API (object cache + Workers Cache) and cache:purge plugin capability feat: cache purge API (object cache + native Workers Caching) Jul 29, 2026
POST /admin/cache/workers and ctx.cache.purgeWorkersCache() accept
optional pathPrefixes (paths or full URLs, normalized). Empty input
still purges everything via cache.purge.
Move URL regex to module scope, drop redundant unknown union, rename
shadowed Tooltip render prop, and include cache:purge in
CAPABILITY_LABELS contract test.
@scottbuscemi
scottbuscemi merged commit e886554 into main Jul 29, 2026
39 of 41 checks passed
@scottbuscemi
scottbuscemi deleted the feat/admin-cache-purge branch July 29, 2026 21:49
@emdashbot emdashbot Bot mentioned this pull request Jul 29, 2026
@scottbuscemi
scottbuscemi restored the feat/admin-cache-purge branch July 29, 2026 21:57
@khoinguyenpham04 khoinguyenpham04 added the bot:review Trigger an emdashbot code review on this PR label Jul 29, 2026

@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.

I reviewed PR #2275 (feat/admin-cache-purge) against AGENTS.md and the full diff.

Approach judgment: the design fits EmDash's existing patterns. Cache invalidation is gated behind an admin permission (settings:manage) and a new plugin capability (cache:purge), exposed through REST handlers that return the standard ApiResult envelope, and routed via the usual Astro integration injection. The Cloudflare-specific cache.purge path is isolated in a new virtual:emdash/workers-cache module, keeping core adapter-agnostic. Both the Cloudflare and workerd sandboxes get parity for the new cache APIs. The Block Kit button additions (disabled/title) are small and localized.

I checked authorization, API envelope shape, Lingui wrapping, SQL safety, index discipline, logged-out route impact, and changeset coverage; nothing concerning there. However, three issues need attention before I'd sign off:

  1. declaredAccessSchema in both packages/core/src/plugins/manifest-schema.ts and packages/plugin-types/src/manifest-schema.ts is missing the new cache category. Because reconcileManifestAccess treats declaredAccess as authoritative, a manifest that spells out cache.purge will silently have that access stripped and the cache:purge capability dropped after validation. This defeats the trust-contract model and breaks capability-change diffing for cache purge.

  2. In packages/core/src/plugins/context.ts, the insertion of createCacheAccess left the existing createUserAccess JSDoc block orphaned immediately above the new function, so createUserAccess now has no docstring and the orphan block documents the wrong function.

  3. normalizeWorkersCachePathPrefix accepts protocol-relative URLs (//example.com/...) and collapses them into a same-site path prefix, so an admin could inadvertently purge the wrong cache prefix.

The rest of the PR is clean and well-scoped.


Findings

  • [needs fixing] packages/core/src/plugins/manifest-schema.ts:294-295

    The PR adds cache:purge to the capability vocabulary and the DeclaredAccess interface, but declaredAccessSchema does not include a cache category. Zod strips unknown keys during parse, so reconcileManifestAccess derives capabilities/allowedHosts from a declaredAccess value that omits cache.purge. A manifest that declares both capabilities: ["cache:purge"] and declaredAccess: { cache: { purge: {} } } will therefore lose cache:purge after validation. Add the cache operation to the trust-contract schema so it round-trips.

    	users: z.object({ read: accessConstraints.optional() }).optional(),
    	cache: z.object({ purge: accessConstraints.optional() }).optional(),
    });
    
  • [needs fixing] packages/plugin-types/src/manifest-schema.ts:269-270

    declaredAccessSchema here is missing the cache category that was added to the DeclaredAccess interface and to the capabilitiesToDeclaredAccess / declaredAccessToCapabilities helpers in the same package. Manifest parsing will strip declaredAccess.cache, causing reconcileManifestAccess to drop cache:purge from the reconciled manifest. Add the matching operation to keep the schema, the type, and the conversion helpers in sync.

    	users: z.object({ read: accessConstraints.optional() }).optional(),
    	cache: z.object({ purge: accessConstraints.optional() }).optional(),
    });
    
  • [needs fixing] packages/core/src/plugins/context.ts:954-961

    The insertion of createCacheAccess left the existing createUserAccess JSDoc block immediately above it, so the createUserAccess docstring now documents createCacheAccess and createUserAccess has no docstring. Remove the orphan block here and re-attach the Create read-only user access... docstring immediately before createUserAccess (around line 998).

    /**
     * Create cache purge access for plugins with `cache:purge`.
     */
    export function createCacheAccess(db: Kysely<Database>): CacheAccess {
    
  • [needs fixing] packages/core/src/api/handlers/workers-cache.ts:56

    normalizeWorkersCachePathPrefix accepts protocol-relative URLs such as //example.com/posts. Because those strings fail the absolute-URL regex, they are treated as bare paths, prepended with /, and have their duplicate slashes collapsed into /example.com/posts. An admin could therefore inadvertently purge the wrong cache prefix. Reject // inputs before the empty-path check.

    	if (trimmed.startsWith("//")) {
    		return { ok: false, message: "Protocol-relative URLs are not allowed" };
    	}
    	if (!trimmed) {
    		return { ok: false, message: "Path is required" };
    	}
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 29, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 29, 2026
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