Skip to content

feat(core): extend plugin runtime capabilities#2018

Merged
ascorbic merged 7 commits into
emdash-cms:mainfrom
ttmx:feat/plugin-runtime-capabilities
Jul 17, 2026
Merged

feat(core): extend plugin runtime capabilities#2018
ascorbic merged 7 commits into
emdash-cms:mainfrom
ttmx:feat/plugin-runtime-capabilities

Conversation

@ttmx

@ttmx ttmx commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Extends the plugin runtime with three capabilities needed by Cloudflare-hosted plugins:

  • Makes database-backed ctx.cron available when no in-process scheduler exists, including plugin API routes and rebuilt hook pipelines. External platform crons could not previously execute plugin scheduled crons, now they can.

  • Exposes scheduledAt through read-only plugin content access so plugins can handle scheduled content.

  • Private plugin routes did not get authenticated with external auth providers (the auth only depended on the astro session), now it also supports external auth providers like CF Access.

The changes are additive and include regression coverage for schedulerless cron contexts, scheduled timestamps, private/public external-authenticated plugin routes, and opaque authentication failures.

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). No admin UI strings are added.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: In talks in internal gchat

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: GPT 5.6 Sol

Screenshots / test output

No visual changes.

Verified locally from a clean frozen-lockfile install:

  • pnpm build
  • pnpm typecheck
  • pnpm lint
  • pnpm format and pnpm format:check
  • pnpm test, including the admin browser suite
  • SQLite-backed cron and content-capability integration tests: 79/79
  • External-auth plugin-route tests: 3/3
  • git diff --check

Not run: the separate end-to-end Playwright suite (pnpm test:e2e).

@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ae8d4ed

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

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

PR template validation failed

Please fix the following issues by editing your PR description:

See CONTRIBUTING.md for the full contribution policy.

@pkg-pr-new

pkg-pr-new Bot commented Jul 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: ae8d4ed

Provide database-backed cron access without an in-process scheduler, expose scheduled publication timestamps through content access, and authenticate private plugin routes with external providers.
@ttmx
ttmx force-pushed the feat/plugin-runtime-capabilities branch from 16ebdd3 to db41fe6 Compare July 13, 2026 15:44
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ttmx
ttmx marked this pull request as ready for review July 16, 2026 08:36
Copilot AI review requested due to automatic review settings July 16, 2026 08:36
@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Jul 16, 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.

This PR adds three useful Cloudflare/plugin-runtime capabilities: DB-backed cron when no in-process scheduler exists, scheduledAt in read-only plugin content access, and external-auth support for private plugin API routes. The approach is additive and fits EmDash’s architecture.

The code is generally careful — tests cover the new schedulerless cron context, scheduledAt reads, and external-auth private/public route behavior. I did not find SQL-safety, locale-filtering, or envelope-shape regressions.

Two things need addressing before merge:

  1. createContentAccessWithWrite returns ContentItem without populating the new scheduledAt field, while the read-only createContentAccess does. That makes the write API inconsistent with the type and with the read API.

  2. The JSDoc for handlePluginRouteAuth still claims it "never block[s] unauthenticated requests," but the new external-auth branch hard-blocks private plugin routes in that mode. The comment is now misleading.

Process note: AGENTS.md requires features to link to a maintainer-approved Discussion. The PR checklist leaves that item unchecked with only an "internal gchat" note; this should be resolved or explicitly waived by a maintainer before merging.


Findings

  • [needs fixing] packages/core/src/plugins/context.ts:402

    The create method of createContentAccessWithWrite builds a ContentItem but omits scheduledAt, even though the repository row (item.scheduledAt) is available and the read-only createContentAccess path maps it. This makes content.create() inconsistent with the updated ContentItem type and with content.get()/content.list().

    					publishedAt: item.publishedAt,
    					scheduledAt: item.scheduledAt,
    				};
    
  • [needs fixing] packages/core/src/plugins/context.ts:459

    Same as the create case: the update method returns a ContentItem without scheduledAt. Add it so write and read paths agree on the shape.

    					publishedAt: item.publishedAt,
    					scheduledAt: item.scheduledAt,
    				};
    
  • [suggestion] packages/core/src/astro/middleware/auth.ts:382

    This JSDoc is no longer accurate: it says plugin-route auth "never block[s] unauthenticated requests," but the new external-auth branch below returns handleExternalAuth(...) for private plugin routes, which returns 401 on any authentication failure. Update the comment to describe the current behavior (soft auth for public routes / Bearer/session, hard auth for private routes under external auth).

     * Plugin-route auth: resolve the user from Bearer token, external provider, or
     * session if present. Public routes are always allowed through; private routes
     * are blocked when credentials are invalid or absent. The catch-all handler enforces
     * the `plugins:manage` permission and CSRF for private invocations.
    

Copilot AI 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.

Pull request overview

This PR extends the core plugin runtime to better support Cloudflare/serverless-hosted plugins by enabling schedulerless cron contexts, exposing scheduled publication timestamps to read-only plugin content access, and ensuring private plugin API routes can authenticate via external auth providers (e.g. Cloudflare Access).

Changes:

  • Wire cronReschedule into rebuilt hook pipelines and plugin route execution so ctx.cron can exist even without an in-process scheduler.
  • Add scheduledAt to the plugin-facing ContentItem shape and populate it from the content repository.
  • Add external-auth support for private plugin API routes in the Astro auth middleware, with unit/integration regression coverage and a changeset.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/core/src/emdash-runtime.ts Ensures cron reschedule context wiring survives hook-pipeline rebuilds and is passed into plugin route registries.
packages/core/src/astro/middleware/auth.ts Adds external-auth handling for private plugin API routes while preserving public-route behavior.
packages/core/src/plugins/types.ts Extends plugin ContentItem type with scheduledAt.
packages/core/src/plugins/context.ts Threads scheduledAt through plugin read-only content access (get/list).
packages/core/tests/unit/astro/plugin-route-external-auth.test.ts Adds unit tests covering external auth behavior for public vs private plugin routes and opaque failures.
packages/core/tests/integration/runtime/plugin-cron-route.test.ts Adds integration coverage for cron availability in plugin routes/hooks when no in-process scheduler exists.
packages/core/tests/integration/plugins/capabilities.test.ts Adds integration coverage ensuring scheduledAt is surfaced through plugin content capabilities.
.changeset/plugin-runtime-capabilities.md Publishes the additive runtime capability changes via a patch changeset.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/emdash-runtime.ts Outdated
Comment thread packages/core/tests/integration/runtime/plugin-cron-route.test.ts Outdated
Comment thread packages/core/src/plugins/types.ts Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Jul 16, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 16, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 16, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 16, 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.

Re-review of #2018: the two blocking code issues from the previous pass are fixed.

  • createContentAccessWithWrite.create() and .update() now populate scheduledAt, so write and read paths return the same ContentItem shape.
  • The handlePluginRouteAuth JSDoc has been updated to describe the new external-auth behavior for private plugin routes.

I re-read the full diff and traced the new behavior across call sites:

  • The schedulerless cronReschedule closure is registered in both the initial pipeline setup and rebuildHookPipeline(), making ctx.cron available to plugins and hook pipelines even when createScheduler is null (e.g. on Cloudflare). The closure correctly resolves to the in-process scheduler when present and a no-op otherwise.
  • Private plugin API routes under external auth now correctly invoke handleExternalAuth(..., true) and return 401 on failure, while public plugin routes and bearer/session paths remain unchanged.
  • scheduledAt is consistently mapped from the existing scheduled_at database column in createContentAccess and createContentAccessWithWrite read/write paths.
  • Tests cover schedulerless cron context, scheduledAt reads, and external-auth public/private route behavior, including opaque failure responses.

No SQL-safety, locale-filtering, envelope-shape, authorization, or index regressions were found. The changeset present-tense lead (Adds …) follows the AGENTS.md guidance.

One non-code process item remains from the previous review: AGENTS.md/CONTRIBUTING.md require a feature PR to link a maintainer-approved Discussion, and the PR checklist still leaves that item unchecked with only an "internal gchat" note. That should be resolved or explicitly waived by a maintainer before merge, but it does not gate the code review itself.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 16, 2026
@ttmx
ttmx requested a review from Copilot July 16, 2026 09:24

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment on lines +381 to +384
* Plugin-route auth: resolve the user from Bearer token, external provider, or
* session if present. Public routes are always allowed through; private routes
* are blocked when credentials are invalid or absent. The catch-all handler enforces
* the `plugins:manage` permission and CSRF for private invocations.

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment on lines +37 to +41
beforeAll(async () => {
vi.stubEnv("DEV", false);
({ onRequest } = await import("../../../src/astro/middleware/auth.js"));
});

Comment on lines +386 to +390
* 2. External provider — only for a *private* route in production external-auth
* mode. Here `handleExternalAuth` is the sole authority (the provider, e.g.
* Cloudflare Access, gates every request and EmDash mints no session of its
* own), so it hard-blocks with 401 on failure. Session auth is deliberately
* NOT a fallback in this case — there is no EmDash session to fall back to.
ttmx added 2 commits July 16, 2026 16:36
Adds an afterAll cleanup (vi.unstubAllEnvs) so the DEV env stub can't leak
into other test files sharing the Vitest worker.
External auth does persist an EmDash session (session.set("user", ...)) so
public pages can identify the user; the comment now says that session is
deliberately not consulted as a fallback on private plugin routes, rather than
implying no session exists.

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment on lines +42 to +43
// Restore env stubs so `import.meta.env.DEV` does not leak into other test
// files sharing this Vitest worker.
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 16, 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.

Re-review of #2018: the two blocking code issues from the previous pass are resolved. createContentAccessWithWrite.create() and .update() now map scheduledAt, so write and read paths return the same ContentItem shape, and the handlePluginRouteAuth JSDoc accurately describes the new external-auth behavior for private plugin routes.

The implementation is additive and fits EmDash's existing architecture:

  • The cronReschedule closure is registered both at runtime startup and in rebuildHookPipeline(), so ctx.cron is available to plugins and hook pipelines even when createScheduler is null (e.g. on Cloudflare). The closure resolves to the in-process scheduler when present and is a no-op otherwise.
  • scheduledAt is consistently mapped from the existing scheduled_at database column in createContentAccess and createContentAccessWithWrite read/write paths.
  • Private plugin API routes under external auth now invoke handleExternalAuth(..., true) and return a 401 on failure, while public plugin routes and bearer/session paths remain unchanged.

I traced the new behavior across call sites and found no logic, SQL-safety, locale-filtering, envelope-shape, authorization, or index regressions. Test coverage is good for schedulerless cron contexts, scheduledAt reads, and public/private external-auth plugin routes.

Two non-blocking items remain:

  1. Process: This is a feature PR, but the maintainer-approved Discussion requirement in CONTRIBUTING.md/AGENTS.md is still unchecked (only noted as "internal gchat"). That should be resolved or explicitly waived by a maintainer before merge.
  2. Test gap: The fix that adds scheduledAt to createContentAccessWithWrite.create()/.update() return values has no regression assertion. Adding one would prevent future shape drift.

Findings

  • [needs fixing] .changeset/plugin-runtime-capabilities.md:5

    This changeset describes a new feature, but CONTRIBUTING.md and AGENTS.md require feature PRs to link a maintainer-approved Discussion. The PR checklist still leaves that item unchecked with only an "internal gchat" note. Resolve the Discussion link or get an explicit maintainer waiver before merge.

  • [suggestion] packages/core/tests/integration/plugins/capabilities.test.ts:436

    The previous review noted that createContentAccessWithWrite.create() and .update() were missing scheduledAt in their returned ContentItem shape; that is now fixed in src/plugins/context.ts. Add a regression assertion so the write-return shape doesn't silently drift again.

    				expect(created.data.title).toBe("New Post");
    				expect(created.scheduledAt).toBeNull();
    

    The same assertion should be added to the update() test below (around line 497) so both write paths verify the field is preserved on return.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 16, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Jul 16, 2026
@ascorbic
ascorbic merged commit 32be60b into emdash-cms:main Jul 17, 2026
49 of 50 checks passed
@emdashbot emdashbot Bot mentioned this pull request Jul 17, 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.

3 participants