Skip to content

test(contract): core and @dune/plugin-api agree on the first slice of the plugin contract - #119

Merged
zumbrunn merged 58 commits into
mainfrom
feat/plugin-api-compat
Nov 10, 2026
Merged

zumbrunn merged 58 commits into
mainfrom
feat/plugin-api-compat

Conversation

@zumbrunn

@zumbrunn zumbrunn commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Update 2026-11-08: stacked on #134 (the contract split and typed capability keys), so the test follows the new surface: the admin and bar types and Contributions are gone from both sides; CapabilityKey, EditorIdentity and InlineEditRequestContext are checked. Retarget to main once #134's stack has merged.

Adds tests/contract/plugin_api_test.ts and the @dune/plugin-api dependency (jsr:@dune/plugin-api@^1, added to the dependency-age exclude list). Stacked on #115 (feat/plugin-app): retarget to main when that merges.

Compile-time assignability in both directions for the app/route-context, storage, content API, search-engine, form, blueprint, inline-edit and scheduled-action types (identical types are checked both ways, handed-out types core -> package, supplied types package -> core), plus a parity check for isDuneError. Negative control verified: a deliberately wrong assertion fails.

Companion: duneorg/plugin-api (first-slice PR).

Hold

CI is red until @dune/plugin-api@1.0.0 is published on JSR: the specifier does not resolve outside a workspace. Verified locally with a workspace link. Do not merge before the package is published and linked to its repo.

🤖 Generated with Claude Code

zumbrunn and others added 8 commits November 6, 2026 08:44
MountApi.app is a PluginApp and route handlers receive a PluginRouteContext;
Fresh's App and FreshContext satisfy both structurally. Handler properties in
the contribution types are methods, so a plugin that needs ctx.render() still
passes a full FreshContext handler.

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

The changelog named only the Fresh-only calls on `app` as breaking. A handler whose
`ctx` is inferred from the contribution type no longer has render() or redirect()
either: it has to be annotated FreshContext. The publicRoutes, pageEditorHandler and
PublicRouteRegistration doc comments now say so instead of promising a full Fresh
context.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the plugin contract

Compile-time assignability in both directions for the app, storage, content
API, search-engine, form, blueprint, inline-edit and scheduled-action types,
plus an isDuneError parity check. Needs @dune/plugin-api published to resolve
outside a workspace.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…@dune/plugin-api

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e match @dune/plugin-api

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he package is narrower

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…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>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@zumbrunn
zumbrunn changed the base branch from feat/plugin-app to main November 6, 2026 22:06
zumbrunn and others added 21 commits November 6, 2026 23:18
…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 12 commits November 8, 2026 12:34
# 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>
…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>
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 11 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>
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>
Brings in the squashed #128-#141. This PR only adds the contract test and the
@dune/plugin-api import, so the result is main plus those two files; every
conflict (the stack's own files, now on main) resolves to main's side.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@zumbrunn
zumbrunn changed the base branch from feat/inline-edit-routes to main November 10, 2026 15:26
@zumbrunn
zumbrunn marked this pull request as ready for review November 10, 2026 16:34
zumbrunn and others added 2 commits November 10, 2026 18:37
Brings in #118 (the theme-api compat test), which edited the same two
deno.json lines. Both contract packages are now excluded from the dependency
age gate and imported.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@zumbrunn
zumbrunn merged commit 951c9eb into main Nov 10, 2026
8 checks passed
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