Skip to content

feat(admin)!: provide auth.session and scheduler.executor, consume capabilities (#97, step 4) - #17

Closed
zumbrunn wants to merge 21 commits into
feat/search-analyticsfrom
feat/capabilities
Closed

zumbrunn wants to merge 21 commits into
feat/search-analyticsfrom
feat/capabilities

Conversation

@zumbrunn

@zumbrunn zumbrunn commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Part of duneorg/dune#97 (step 4) and #64. Stacked on #16 (slice 3b); base is that branch. Draft until the admin stack (#12 to #16) and core 0.36 are out; it also needs the core capability PRs (duneorg/dune#109, #110, #111).

The admin now meets core through the typed capability registry instead of the untyped adminContext bag, adminServices/adminPages and mountEarly.

Provides (declared in setup(), resolved by core before mount()):

  • auth.session: who is the admin on a request. Core uses it for the admin bar's response transform and the inline-edit WebSocket's admin fallback.
  • scheduler.executor: runs due scheduled actions; dune serve starts core's scheduler loop when it exists.
    Both delegate lazily to the admin context built in mount() and answer "nobody" (unauthenticated, no-op) before that.

Consumes (MountApi.capabilities): the inline-edit and editor.content slots (falling back to the built-in block editor), and the admin.pages contributions. The jobs view reads BootstrapResult.jobScheduler, which dune serve sets after mount.

Removed: bootstrap.adminContext = ..., the mountEarly implementation and mountDuneAdminEarly(). mountDuneAdmin(app, ctx, adminContext?) falls back to the context mount() stored, not ctx.adminContext.

permission is required on admin.pages and admin.routes entries (core types it that way). The admin enforces it at mount too: an entry without one, with an unknown method or with a path that does not start with / is skipped and logged (admin.pages.invalid / admin.routes.invalid), so an untyped plugin cannot expose a page or a mutation endpoint to every authenticated admin. The pages mounting moved into mountPluginAdminPages() so it is unit-tested.

Guarded plugin routes: admin.routes. Routes a plugin registers in its own mount() are no longer behind the admin's middleware (that is what mountEarly provided), so withGuards() cannot work there. The admin now mounts contributions to the new admin.routes point ({ method, path, permission, handler }) under its prefix behind its own login, the CSRF check on non-GET methods and the permission check (mountPluginAdminRoutes()); the handler only runs when all pass. Invalid entries are skipped and logged as admin.routes.invalid.

auth.session also declares sessionCookies: ["dune_session"] so core can decide when to authenticate a public page and when to keep a response out of the shared page cache.

Tests. deno task check and deno test -A --unstable-kv pass (254) against the core capability stack in a scratch workspace. New: tests/admin/capabilities_e2e_test.ts runs against real core: the admin provides both capabilities (and anonymous is reported unauthenticated), an admin.pages page contributed by another plugin is mounted behind the login, and a plugin's editor.content slot is picked up. Removed: the mountEarly ordering test.

Conformance. tests/admin/conformance_test.ts runs @dune/testing's runAdminConformance against the real admin (temp site, seeded user, real login): all 9 checks pass. Needs @dune/testing released with the suite (deno.json pins jsr:@dune/testing@^1).

Page events (#97). Whoever writes a page fires its event; core does not. Six admin paths wrote pages and fired nothing: history restore, staging publish, both translate routes, dev apply and collab auto-save (reorder renamed pages and had no event at all). They now share announcePageEvent() (hook plus the configured content webhooks); reorder fires the new onPageMove; collab auto-save fires onPageUpdate when a save changed the file (hook only, it saves every few seconds). New tests/admin/page_events_test.ts drives create, update, reorder, delete and restore through the real routes; the conformance driver gained createPage / deletePage. 265 tests. Needs core with onPageMove (duneorg/dune#112). Not tested over HTTP: staging publish (its route is unreachable, being fixed separately), translate, dev apply and collab auto-save.

mount() reads MountApi.runtime (#97, core#114). Core's mount() argument no longer carries the whole bootstrap result; the admin takes engine, search, history, hooks, submissions, flex, image cache, metrics, sections, authz and the job scheduler from the curated PluginRuntime (authz replaces the old authz && authzAdapter check). mount() now calls a new mountAdminPanel() (panel routes, context middleware, admin.pages / admin.routes) and no longer registers public plugin routes or form endpoints a second time, since core's createDuneApp() owns those; the headless mountDuneAdmin(app, bootstrapResult) is unchanged. 267 tests pass against core#114 plus the mutation-events commits.

Narrows the contract's PluginApp to Fresh (#97, core#115). Core's contract no longer names Fresh types: MountApi.app is a PluginApp, handlers get a PluginRouteContext. The admin uses Fresh-only features (fsRoutes, layout, route), so mount() casts app to Fresh's App and the block editor declares pageEditorHandler(ctx: FreshContext). No behaviour change. deno task check now also type-checks mod.ts, which it silently skipped (that is how the cast error was found late). 267 tests pass.

Slice 4 (#97). mountDuneAdmin() no longer registers plugins' public routes or the form/webhook endpoints, and the createSubmissionManager re-export is gone. Core's createDuneApp() already registers both (the admin's second call was a no-op), and the submission store is PluginRuntime.submissions. The admin now imports neither registerPluginPublicRoutes nor registerFormRoutes, which lets core stop exporting them later. Works with core as it is today (267 tests). Note: the documented 'headless' flow (own App + mountDuneAdmin(), never mountPlugins()) already 500s on every admin route because the admin context is never set; mountDuneAdmin() is left as a thin wrapper and that flow is not fixed here.

Not mergeable until @dune/core is ^0.36.0. Still to do for step 4: plugin-inline-edit (provides inline-edit, reads bar.actions).

🤖 Generated with Claude Code

zumbrunn and others added 7 commits November 5, 2026 11:55
…pabilities (#97, step 4)

Admin no longer fills core's adminContext bag or implements mountEarly:
it declares auth.session and scheduler.executor in setup(), reads inline-edit,
editor.content and admin.pages through MountApi.capabilities, and reads the job
scheduler from BootstrapResult.jobScheduler. mountDuneAdminEarly() is removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ssion checks (#97)

A plugin contributes { method, path, permission?, handler } and the admin
registers it under its prefix; the handler only runs after the admin login, the
CSRF check on non-GET methods and the permission check. Replaces registering
guarded mutation routes in mount() with withGuards(), which stops working now that
the admin no longer mounts first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An entry without a permission is skipped and logged instead of being exposed to
every authenticated admin. The pages mounting moves into mountPluginAdminPages()
so it can be tested.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Restore, staging publish, translate x2, dev apply, reorder (new onPageMove) and
collab auto-save wrote pages and fired nothing. They now share
announcePageEvent(), which fires the hook and the content webhooks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
zumbrunn and others added 13 commits November 5, 2026 21:20
… result (#97)

Adds mountAdminPanel() (panel only); mount() no longer re-registers public
plugin routes and form endpoints, which createDuneApp() owns.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
mount() casts app to Fresh's App (the admin uses fsRoutes, layout, route) and
the block editor declares a FreshContext handler. deno task check now also
type-checks mod.ts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s; drop the createSubmissionManager re-export (#97, slice 4)

Core's createDuneApp() owns both registrations (the admin's second call was a
no-op), and the submission store is PluginRuntime.submissions. Lets core stop
exporting registerPluginPublicRoutes, registerFormRoutes and createSubmissionManager.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Both authenticate() implementations threw, and mount() refused ldap/saml config
anyway. Custom providers still go through the authProvider plugin option.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…arries it

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…in.runtime_dir

Core moved runtime_dir and max_revisions to system.* because its own history,
job state and analytics use them. The admin takes the resolved value from
services.paths.runtimeDir (webhook logs, email preview, CSRF secret, workflow
and page events). /api/introspect reports pageCacheEnabled: core no longer has
system.cache.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Core stops reading admin.path and learns where the admin is mounted from this
slot. With the admin disabled nothing is provided and core treats /admin as an
ordinary path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
AuditLogger, the audit types and the DeepL/Google/LibreTranslate translators move
here from @dune/core (the @dune/core/audit and @dune/core/mt entry points are gone).
The admin builds both in setup(); translators take the fetch they use, and the
admin passes services.security.safeFetch so core's SSRF policy still applies.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e site facts it cannot get from PluginApi

createAdminPlugin(opts) no longer takes config and storage (they come from PluginApi in setup()),
and the default export is the plugin-loader factory (config, { root, dev }) so
plugins: [{ src: jsr:@dune/plugin-admin@^4 }] is enough. admin.enabled is gone (core refuses it:
list the admin to run it). Tests list the admin explicitly; the conformance driver uses the
2.0.0 @dune/testing shape (hostsContributions, prefix from the admin.prefix slot).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…gin config

name dune-admin -> admin, so the settings are config.plugins.admin (the config: of its entry in
plugins:). adminSettings(config) is the one reader; it applies the defaults and refuses a path
or auth_provider that would desync the admin. WebhookEndpointConfig, WebhookContentEvent and
AuthProviderConfig move here from @dune/core, which now has no admin config at all.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Read from config.plugins.admin.machine_translation with api_key / base_url (were apiKey / baseUrl).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Matches @dune/core's rename (duneorg/dune#130): the resource names the site
content being protected, not the panel.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…une#125)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@zumbrunn

Copy link
Copy Markdown
Member Author

Landed in 4.0.0 as part of #26 (one squash of the whole stack: its intermediate commits only type-check against the final contract). Verified against the published @dune/core 0.36.0 and @dune/testing 2.0.0.

@zumbrunn zumbrunn closed this Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant