Skip to content

feat(plugin-api)!: core gates /api/inline-edit/*; typed capability keys replace declaration merging - #134

Merged
zumbrunn merged 43 commits into
mainfrom
feat/inline-edit-routes
Nov 10, 2026
Merged

zumbrunn merged 43 commits into
mainfrom
feat/inline-edit-routes

Conversation

@zumbrunn

@zumbrunn zumbrunn commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Safeguards added here: a live editing session now respects page ownership (an author joins a session only on a page it created; admin/editor any page), and core logs auth.automatic_admin_tier_grant / dune doctor --boot prints an access line wherever the config assigns an admin-tier role by itself (a payment product's role, an external-jwt login). Informational only; nothing is refused.

Part of #127 (slice 2). Stacked on #133.

  • Core resolves who is editing (createEditorResolver: a site user, else the editor.session user, both needing pages.update by authz), checks that writes are same-origin, and hands /api/inline-edit/* to the new InlineEditManager.handleRequest. The WebSocket and the bar use the same resolver, so a signed-in site user who may edit gets the bar, and signed-in requests bypass the shared page cache. This lives in core, after the site login runs, because plugin mount() routes run before that middleware and cannot see site users.
  • InlineEditManager loses commit/patchFields; page events document an optional actor.
  • Extension points are CapabilityKey<T> tokens (capabilityKey(name)), not declare module augmentation: deno publish rejects ambient modules. Contributions is removed and Capabilities stays closed.

Draft: breaking; the sibling PRs (plugin-admin, plugin-inline-edit, plugin-api, dune-docs) move together.

🤖 Generated with Claude Code

zumbrunn and others added 21 commits November 6, 2026 22:47
…dmin settings

Core's history engine, job state, search analytics and public sessions use the
runtime directory, and the history engine uses max_revisions, so they cannot
live under admin: when the admin is optional. They are system.runtime_dir and
system.max_revisions now, resolved by resolveRuntimeDir() and
resolveMaxRevisions(). The old admin.* keys, in both spellings, are refused with
the new location (step 1 of the admin config plan).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ix slot) (#123, step 2)

Core no longer reads admin.path. The admin plugin provides the admin.prefix
capability slot; core reads it lazily for page-cache and transform exclusion,
onRequest short-circuit protection, plugin route reservation, hybrid static
output and the dev/serve banners. With no provider there is no admin and no
path is treated as one. resolveCapabilities() resolves slots idempotently for
callers that run before createDuneApp(). MCP stops reporting admin.path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…st (#123, step 4)

bootstrap() stops importing @dune/plugin-admin itself. The admin runs only when site.yaml
lists it under plugins:; dune new writes the entry and dune migrate:config adds it and
removes admin.enabled. admin.enabled and admin.authz_store are removed, the authProvider
bootstrap option and the admin version guard are gone, the authz system is created when a
plugin provides auth.session or declares authzActions, plugin factories receive
(config, { root, dev }), multisite builds its island snapshot from the sites' plugins,
and dune doctor keys its admin check on admin.prefix.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…#123, step 3)

services.engines.mt/audit, PluginServicesOptions.root, SiteConfig.machine_translation and
the @dune/core/mt and @dune/core/audit entry points are removed; @dune/plugin-admin owns
them now. The response-transforms test stops importing types from the published admin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t core's (#123, step 5)

DuneConfig.admin and its types, defaults, schema entry and validation are removed; a system.yaml admin:
block is refused. The settings live under config: of the admin's entry in plugins: and dune migrate:config
moves them (lifting runtime_dir, max_revisions and data_dir to the top level). MCP stops reporting admin
settings and drops the audit-log resource.

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

site.machine_translation is the admin's setting: migrate:config moves it under config: of the admin's
entry as machine_translation, renaming apiKey/baseUrl to api_key/base_url; a site.yaml that still has it
is refused.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The authz system now exists whenever the site has a login (site.auth), a
plugin provides auth.session, or a plugin declares authzActions, and
mountDuneAuth reuses that one instance. admin, editor and author are granted
once at group level (group:<role> holds <role> on app:site), so the role
membership site auth already syncs is enough: no per-user tuple, no admin
plugin. The resource is renamed from app:admin to app:site; tuples are derived
data and re-seeded at every start.

Closes #130

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t once (#130)

Group membership, which page gating and the admin-tier role grants read, was
only seeded at startup, so any change to roles[] waited for a restart. User
stores that core creates are now wrapped with withRoleSync: create, a roles
update and delete make member-of-group tuples match the record. The CLI role
commands sync the same way, and the payments webhook stops writing the
membership twice on a synced store.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
main squash-merged #124, #126 and #125 (rebased), so this branch's history no
longer matched its base. The tree is #128's own change replayed onto main, with
the slot renamed to editor.session and the doctor check keyed on admin.prefix.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The base was squash-merged into main (and this stack rebased), so history no
longer matched. The tree is this PR's own change replayed onto the new base.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The base was squash-merged into main (and this stack rebased), so history no
longer matched. The tree is this PR's own change replayed onto the new base.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The base was squash-merged into main (and this stack rebased), so history no
longer matched. The tree is this PR's own change replayed onto the new base.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lare their own extension points (#127)

AdminPageRegistration, AdminRouteRegistration, ContentEditorPlugin and the bar
action types leave core's contract along with the keys admin.pages,
admin.routes, editor.content and bar.actions: core never read them. Capabilities
and Contributions are open to declaration merging, keyed by the name of the
plugin that consumes the key. ResponseTransformContext loses barActions and
adminPrefix; a bar-owning plugin reads them through MountApi.capabilities.

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

Replaces barActions and adminPrefix with the registry's reader, so a transform
asks for what it needs per request and a plugin module shared between sites keeps
no state of its own.

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…o the editor.inline provider (#127)

Core resolves who is editing (a site user, else the editor.session provider's
user; both need pages.update by authz.check), checks the origin of writes, and
passes the request to InlineEditManager.handleRequest. The WebSocket and the
bar's response transform use the same resolver, so a signed-in site user gets
the bar too, and their requests bypass the shared page cache.

InlineEditManager drops commit/patchFields (core never called them; the admin's
content routes did) and gains handleRequest. Page events document an optional
actor. checkInlineEditPermission* are replaced by createEditorResolver.

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…on (#127)

JSR refuses 'declare module' in a published package, so plugin-owned extension
points are CapabilityKey<T> tokens (capabilityKey(name)) that provide, contribute,
get and list accept. Capabilities stays closed; Contributions is removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
zumbrunn and others added 7 commits November 8, 2026 12:32
…mounts plugins; drop unused admin import-map entries; warn on per-environment admin.enabled

headless-smoke set admin.enabled: false, which core now refuses. The headless main.ts
imported the admin's mount subpath unversioned; it now calls mountPlugins and takes island
specifiers from the loaded plugins.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ookup, document exact membership sync, note orphaned app:admin tuples

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	src/cli/migrate-config.ts
#	tests/cli/migrate_config_test.ts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
zumbrunn and others added 3 commits November 8, 2026 12:38
…config assigns admin-tier roles automatically

An author could join the inline-edit WebSocket on a page it did not create; the commit was refused
but the shared draft was not. mayEditPage applies the commit rule at the socket.
automaticAdminTierGrants names the config paths that assign admin, editor or author by
themselves (a payment product's role, an external-jwt login): a start-up log line and a
dune doctor --boot access finding, informational, never refusing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
zumbrunn and others added 3 commits November 8, 2026 15:49
Fresh's BuildOptions has no islandSpecifiers, so the option the headless
main.ts passed to the Builder constructor was silently dropped and no
plugin island (the admin's included) was registered. Register each
specifier after construction, as dev.ts and serve.ts do.

Fixes #140

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Brings in the squashed #128 and the #135 doc note. The one conflict was
createAuditTrail in plugin-api/create-services.ts: this branch moves the
audit trail out of core, so its side wins and the block stays removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
zumbrunn and others added 7 commits November 10, 2026 14:01
…ver write a lifted key twice

Two bugs in the admin-settings move:

- A 0.35 site with `admin.enabled: false` had its whole admin: block
  deleted, including runtime_dir, max_revisions and data_dir, which are
  core settings now lifted to the top level for every other site. They
  are lifted here too. Keys that belong to forms or webhooks (honeypot,
  notifications, incoming_webhooks) make it a manual move, as for an
  enabled admin, instead of being deleted.
- A lifted key already present at the top level of system.yaml was
  appended a second time, which @std/yaml refuses to parse. An equal
  value is skipped; a different one is reported as a manual move and the
  files are left untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Brings in the squashed #128-#132. The one conflict was the CHANGELOG
entry for the contract split, kept as this branch has it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@zumbrunn
zumbrunn marked this pull request as ready for review November 10, 2026 13:38
zumbrunn and others added 2 commits November 10, 2026 14:38
Brings in the squashed #133. Its tree is identical to the contract-split
head this branch already merged, so every conflict resolves to this
branch's side and the merge changes nothing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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