feat: cache purge API (object cache + native Workers Caching) - #2275
Conversation
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 detectedLatest commit: 5e2fe09 The changes in this PR will be included in the next version bump. This PR includes changesets to release 20 packages
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 |
Scope checkThis 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. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | 5e2fe09 | Jul 29 2026, 10:07 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 5e2fe09 | Jul 29 2026, 10:01 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 5e2fe09 | Jul 29 2026, 10:01 PM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
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).
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.
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.
There was a problem hiding this comment.
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:
-
declaredAccessSchemain bothpackages/core/src/plugins/manifest-schema.tsandpackages/plugin-types/src/manifest-schema.tsis missing the newcachecategory. BecausereconcileManifestAccesstreatsdeclaredAccessas authoritative, a manifest that spells outcache.purgewill silently have that access stripped and thecache:purgecapability dropped after validation. This defeats the trust-contract model and breaks capability-change diffing for cache purge. -
In
packages/core/src/plugins/context.ts, the insertion ofcreateCacheAccessleft the existingcreateUserAccessJSDoc block orphaned immediately above the new function, socreateUserAccessnow has no docstring and the orphan block documents the wrong function. -
normalizeWorkersCachePathPrefixaccepts 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-295The PR adds
cache:purgeto the capability vocabulary and theDeclaredAccessinterface, butdeclaredAccessSchemadoes not include acachecategory. Zod strips unknown keys during parse, soreconcileManifestAccessderives capabilities/allowedHosts from adeclaredAccessvalue that omitscache.purge. A manifest that declares bothcapabilities: ["cache:purge"]anddeclaredAccess: { cache: { purge: {} } }will therefore losecache:purgeafter validation. Add thecacheoperation 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-270declaredAccessSchemahere is missing thecachecategory that was added to theDeclaredAccessinterface and to thecapabilitiesToDeclaredAccess/declaredAccessToCapabilitieshelpers in the same package. Manifest parsing will stripdeclaredAccess.cache, causingreconcileManifestAccessto dropcache:purgefrom 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-961The insertion of
createCacheAccessleft the existingcreateUserAccessJSDoc block immediately above it, so thecreateUserAccessdocstring now documentscreateCacheAccessandcreateUserAccesshas no docstring. Remove the orphan block here and re-attach theCreate read-only user access...docstring immediately beforecreateUserAccess(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:56normalizeWorkersCachePathPrefixaccepts 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" }; }
What does this PR do?
Adds a first-party way for admins and sandboxed plugins to clear EmDash caches.
CMS object cache (KV / memory)
GET/POST/_emdash/api/admin/cache/object(settings:manage)ctx.cache.getObjectCacheStatus()/ctx.cache.purgeObjectCache()Workers Caching (native edge page cache)
GET/POST/_emdash/api/admin/cache/workers(settings:manage)cache.purgeis availablecache.purge({ purgeEverything: true })orcache.purge({ pathPrefixes })ctx.cache.getWorkersCacheStatus()/ctx.cache.purgeWorkersCache({ pathPrefixes? })cache.purgeviavirtual:emdash/workers-cache(no zone ID / API token)configured: truewhencache.purgeis a function (production Workers); local workerd may exposecachewithoutpurgeShared
cache:purgegates both in-process and through Cloudflare / workerd sandbox bridgesdisabledandtitle(tooltip)Closes #
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
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):
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.