Skip to content

Core still carries the admin's extension API: decide which slots belong in the frozen plugin contract #127

Closed

Description

@zumbrunn

Follow-up to #123. Steps 1 to 5 of #123 make the admin an ordinary plugin as far as loading and config go. This issue is about what is left in the plugin contract after that. Counts below are from src/ at the head of #126; a file with many mentions is not necessarily coupling (the string "admin" is also a role name).

1. Slots and types whose only consumer is the admin

src/plugin-api/capabilities.ts documents these with the admin as their consumer:

  • admin.pages ("Consumer: the admin")
  • admin.routes ("Consumer: the admin, which applies authentication, CSRF and permission checks")
  • editor.content ("A replacement page editor. Consumer: the admin")

src/hooks/types.ts (the file with the most mentions, 74) carries the matching types: AdminPageRegistration, AdminRouteRegistration, ContentEditorPlugin, AdminBarAction*.

If the admin were a normal plugin, these would be its extension API, defined in its own package, not part of the @dune/plugin-api contract we are about to freeze. Today core ships and freezes an extension API for a single plugin.

2. The bar knows the admin's URL layout

adminPrefix is a field of ResponseTransformContext and AdminBarActionContext (hooks/types.ts). plugin-inline-edit (src/bar.ts) builds ${adminPrefix}/pages/edit?path=... and ${adminPrefix}/api/content/<path>/commit and /fields from it. So the dependency is the admin's page URL and content API, not only its prefix.

3. Admin-shaped code in the request path

src/cli/response-transforms.ts (31 mentions), src/runtime/server.ts (43), src/runtime/register-middleware.ts (21), src/runtime/bootstrap.ts (59). Part of this goes away with #123; the rest should be checked file by file.

Left alone on purpose: identity vocabulary

The admin role, the pages.* / users.* / config.* permissions, DUNE_ADMIN_* and the users:* commands (authz-schema.ts, bootstrap-admin.ts, users-role.ts, users-create.ts). The identity-unification decision made users and RBAC core. Listed so it is a decision and not an oversight; say if it should be revisited.

To decide

  1. Which slots and contribution points are core's contract (core consumes them: auth.session, scheduler.executor, bar.actions, the editor.inline WebSocket side) and which are the admin's own extension API (admin.pages, admin.routes, editor.content)?
  2. If the admin owns its extension points, how does it declare them? The 1.0 contract currently says first-party keys only and third-party keys are unsupported, so this needs either declaration merging or a plain plugin-to-plugin dependency on the admin's types.
  3. How does the bar get its links without knowing the admin's routes (contributions with absolute URLs, or an editor.inline-side API)?

Timing

These change the surface @dune/plugin-api freezes, so they should be decided before it is published.

Not in scope: more than one admin per site (decided: no; auth.session stays exclusive, #125).

Activity

  1. zumbrunn commented on Nov 6, 2026

    @zumbrunn
    MemberAuthor

    Additions after going through how auth.session and authz are actually used (read from origin/main; not reproduced at runtime).

    4. auth.session is the editor session, under a general-sounding name

    Core uses it for: the admin bar and whether editing markers survive in public HTML (register-middleware.ts), the /api/inline-edit/ws fallback (server.ts), and deciding which requests bypass the shared page cache (sessionCookies). Public visitors sign in through the site auth (mountDuneAuth, siteUser), a different path. The contract text says so too ("Who is the admin on this request", "an admin-tier session"). Decide whether the slot should be named for what it is (e.g. editor.session) before the freeze, or whether it should disappear (see 6).

    5. The authz system is tied to the admin or an auth.session provider

    bootstrap.ts creates ctx.authz only when admin.enabled !== false or some plugin provides auth.session. The authz system should exist because something uses it: the admin, another plugin, or the site's own login page.

    Consequence, from the code: a site with public auth (site.auth, e.g. the password login in #117), admin.enabled: false, no auth.session provider and the inline-edit plugin ends up with ctx.authz === undefined. mountDuneAuth then builds its own private bundle (auth/mount.ts, used for page gating) and never assigns it back. checkInlineEditPermissionForSiteUser(ctx.authz, ...) in server.ts fails closed, so every site user gets 403 on /api/inline-edit/ws. Worth a reproduction.

    Smaller: the resource that pages.update is checked against is { type: "app", id: "admin" } (server.ts), even for a site user with no admin in sight.

    6. Acceptance for the decoupling

    A site without the admin plugin and with plugin-inline-edit should work: log in through the site's login page, edit inline, save. The only thing missing should be the bar buttons that link to the admin's page editor.

    What stops that today:

    • the bar saves through ${adminPrefix}/api/content/<path>/commit and /fields, which are the admin's routes (see 2);
    • the ctx.authz gap in 5;
    • the bar and the contexts need an adminPrefix string even when there is no admin.

    So the commit/fields endpoints (and their permission check) need to live in core or in plugin-inline-edit, driven by authz.check("pages.update") on the signed-in site user, and the admin's page-editor links should be an optional bar action the admin contributes. That would also settle question 1: the extension points the admin owns are the ones other plugins reach the admin through.

  2. zumbrunn commented on Nov 6, 2026

    @zumbrunn
    MemberAuthor

    The authz part of point 5 is now reproduced and filed separately as #130 (it has a second cause: a site user's role does not grant pages.update without the admin plugin's app:admin tuple).

  3. zumbrunn commented on Nov 10, 2026

    @zumbrunn
    MemberAuthor

    Decided and done. (1) Core's contract keeps only what core consumes (editor.session, editor.inline, admin.prefix, scheduler.executor); admin.pages, admin.routes, the page-editor slot and the bar-action types left it (#133). (2) The plugin that owns an extension point exports it as a typed CapabilityKey<T> (ADMIN_PAGES, ADMIN_ROUTES, ADMIN_EDITOR, BAR_ACTIONS), because module augmentation is refused by deno publish (#134). (3) The bar no longer knows the admin's URLs: the inline-edit provider serves its own /api/inline-edit/* routes behind core's gate and the admin contributes its own bar.actions (#134, plugin-admin#21, plugin-inline-edit#7). Point 4 became editor.session (#125); the authz part of point 5 was #130 (#132). The admin-shaped code left in the request path is the generic admin.prefix handling (which paths core keeps out of the page cache and away from plugin routes); adminContext and friends are gone. The identity vocabulary (admin role, permissions, DUNE_ADMIN_*, users:*) stays, as decided.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions