From 3a06838192a6dbcec7f237ce96f0d47645bcadc1 Mon Sep 17 00:00:00 2001 From: zumbrunn Date: Fri, 6 Nov 2026 23:28:43 +0100 Subject: [PATCH 1/6] feat(conformance)!: read the admin's prefix from the admin.prefix slot The driver's adminPrefix option is gone: core no longer assumes /admin, so an admin must provide the admin.prefix capability, and the suite reads the prefix from it. adminPrefix?: string becomes hostsContributions?: boolean (it only enables the admin.pages / admin.routes checks). New checks: the admin provides admin.prefix, core reserves it against plugin routes, and with no admin /admin is an ordinary path. The stub admin provides admin.prefix. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 11 ++++ README.md | 11 +++- src/conformance.ts | 98 ++++++++++++++++++++++++++++-- src/stub-admin.ts | 1 + tests/conformance_negative_test.ts | 53 ++++++++++++++++ 5 files changed, 165 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b75194..0c6f907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## [Unreleased] +### Changed — BREAKING + +- **The conformance driver has no `adminPrefix`.** The admin's URL prefix comes + from the `admin.prefix` capability slot, which an admin must now provide (core + no longer assumes `/admin`). `adminPrefix?: string` is replaced by + `hostsContributions?: boolean`, which only enables the `admin.pages` / + `admin.routes` checks. New checks: the admin provides `admin.prefix` (one + provider, a usable prefix), core reserves that prefix against plugin routes, + and with no admin `/admin` is an ordinary plugin path. The stub admin provides + `admin.prefix` (`/stub-admin` by default). Needs `@dune/core` 0.36 (dune#123). + ### Added - **`runAdminConformance()`** (`@dune/testing/conformance`): the admin diff --git a/README.md b/README.md index d0eb78c..eb5ec1c 100644 --- a/README.md +++ b/README.md @@ -119,9 +119,13 @@ app with your admin, how to sign an editor in) and it registers these tests: - creating a page fires `onPageCreate` exactly once and deleting one fires `onPageDelete` exactly once, each with the path relative to the content directory (`createPage`, `deletePage`); +- the admin provides `admin.prefix` (where it is mounted: one provider, a path + starting with `/` and with no trailing slash), and core reserves that prefix + against plugin routes; - `admin.pages` and `admin.routes` contributed by another plugin are mounted - behind the login, the CSRF check and the permission (`adminPrefix`); -- with no admin at all the site serves and nothing admin-related exists. + behind the login, the CSRF check and the permission (`hostsContributions`); +- with no admin at all the site serves, nothing admin-related exists, and + `/admin` is an ordinary path for plugins. ```ts runAdminConformance({ @@ -134,7 +138,8 @@ runAdminConformance({ ``` The optional hooks (`signInViewer`, `editPage`, `createPage`, `deletePage`, -`adminPrefix`) enable the checks that need them. +`hostsContributions`) enable the checks that need them. The admin's prefix is +not a driver option: the suite reads it from the `admin.prefix` slot. `adminConformanceChecks(driver)` returns the checks without registering them, which is how the package tests that the suite rejects a broken admin. The stub admin (`createStubAdmin()`) is the reference run. diff --git a/src/conformance.ts b/src/conformance.ts index a9d2032..56a0a68 100644 --- a/src/conformance.ts +++ b/src/conformance.ts @@ -104,10 +104,12 @@ export interface AdminConformanceDriver { page: { sourcePath: string }, ): Promise; /** - * Optional: the admin's URL prefix, if it mounts `admin.pages` and - * `admin.routes` contributions from other plugins. Enables those checks. + * Optional: true if the admin mounts `admin.pages` and `admin.routes` + * contributions from other plugins. Enables those checks. The admin's URL + * prefix is not part of the driver: the suite reads it from the + * `admin.prefix` slot the admin must provide. */ - adminPrefix?: string; + hostsContributions?: boolean; } const CONTENT = { @@ -166,6 +168,16 @@ function probe() { return { plugin, seen }; } +/** Build an app just to read the prefix the admin provides. */ +async function withPrefix(driver: AdminConformanceDriver): Promise { + let prefix = ""; + await withApp(driver, {}, (app) => { + prefix = adminPrefixOf(app); + return Promise.resolve(); + }); + return prefix; +} + async function withApp( driver: AdminConformanceDriver, options: Partial, @@ -184,6 +196,13 @@ async function withApp( } } +/** The prefix the admin provides through `admin.prefix`; throws if it provides none. */ +function adminPrefixOf(app: ConformanceApp): string { + const slot = app.bootstrap.capabilities.get("admin.prefix"); + assertExists(slot, "no plugin provides admin.prefix"); + return slot.prefix; +} + const cookieNames = (cookie: string) => cookie.split(";").map((c) => c.trim().split("=")[0]).filter(Boolean); @@ -227,6 +246,48 @@ export function adminConformanceChecks( }), ); + t( + "provides admin.prefix: a path prefix core can reserve, from exactly one provider", + () => + withApp(driver, {}, (app) => { + const providers = app.bootstrap.capabilities.providers(); + const slot = providers.filter((p) => p.name === "admin.prefix"); + assertEquals(slot.length, 1, "exactly one admin.prefix provider"); + assertEquals(slot[0].status, "active"); + const prefix = adminPrefixOf(app); + assertEquals( + prefix.length > 1 && prefix.startsWith("/") && !prefix.endsWith("/"), + true, + `"${prefix}" must start with "/" and have no trailing slash`, + ); + return Promise.resolve(); + }), + ); + + t( + "core reserves the admin prefix: a plugin public route under it is refused", + async () => { + const prefix = await withPrefix(driver); + const squatter: DunePlugin = { + name: "conformance-squatter", + version: "1.0.0", + hooks: {}, + publicRoutes: [{ + path: `${prefix}/__conformance-squat`, + handler: () => new Response("squatted"), + }], + }; + await withApp(driver, { extraPlugins: [squatter] }, async (app) => { + const res = await app.request(`${prefix}/__conformance-squat`); + assertEquals( + (await res.text()).includes("squatted"), + false, + "a plugin route answered inside the admin's prefix", + ); + }); + }, + ); + t( "the public site serves, and anonymous visitors are not admins", () => @@ -476,9 +537,7 @@ export function adminConformanceChecks( ); } - if (driver.adminPrefix) { - const prefix = driver.adminPrefix; - + if (driver.hostsContributions) { t( "admin.pages from another plugin are mounted behind the admin's login", async () => { @@ -496,6 +555,7 @@ export function adminConformanceChecks( }, }; await withApp(driver, { extraPlugins: [contributor] }, async (app) => { + const prefix = adminPrefixOf(app); const cookie = await driver.signInEditor(app); const anonymous = await app.request(`${prefix}/__conformance`); assertEquals( @@ -537,6 +597,7 @@ export function adminConformanceChecks( }, }; await withApp(driver, { extraPlugins: [contributor] }, async (app) => { + const prefix = adminPrefixOf(app); const cookie = await driver.signInEditor(app); const url = `${prefix}/api/__conformance/act`; const post = (headers: Record) => @@ -574,11 +635,36 @@ export function adminConformanceChecks( withApp(driver, { withAdmin: false }, async (app) => { assertEquals((await app.request("/")).status, 200); assertEquals(app.bootstrap.capabilities.get("auth.session"), undefined); + assertEquals(app.bootstrap.capabilities.get("admin.prefix"), undefined); assertEquals(app.bootstrap.authz, undefined); assertEquals((await app.request("/api/inline-edit/ws")).status, 501); }), ); + t( + "without the admin nothing is reserved: /admin is an ordinary path for plugins", + () => { + const open: DunePlugin = { + name: "conformance-open", + version: "1.0.0", + hooks: {}, + publicRoutes: [{ + path: "/admin/__conformance-open", + handler: () => new Response("open"), + }], + }; + return withApp( + driver, + { withAdmin: false, extraPlugins: [open] }, + async (app) => { + const res = await app.request("/admin/__conformance-open"); + assertEquals(res.status, 200); + assertEquals(await res.text(), "open"); + }, + ); + }, + ); + return checks; } diff --git a/src/stub-admin.ts b/src/stub-admin.ts index f79b2b2..fe70b81 100644 --- a/src/stub-admin.ts +++ b/src/stub-admin.ts @@ -146,6 +146,7 @@ export function createStubAdmin(options: StubAdminOptions = {}): StubAdmin { })(); state = { users, sessions: null, passwords: services.passwords, ready }; + capabilities.provide("admin.prefix", () => ({ prefix })); capabilities.provide("auth.session", () => ({ ...(declareCookie ? { sessionCookies: [cookieName] } : {}), authenticate, diff --git a/tests/conformance_negative_test.ts b/tests/conformance_negative_test.ts index 1de0a68..3431719 100644 --- a/tests/conformance_negative_test.ts +++ b/tests/conformance_negative_test.ts @@ -95,6 +95,59 @@ Deno.test("conformance catches an admin that provides no auth.session", async () ); }); +/** Run the stub's setup, passing every `provide` through `rewrite` first. */ +const rewriteProvides = ( + rewrite: (name: string, factory: unknown) => [string, unknown] | null, +): Breaker => +(admin) => ({ + ...admin, + setup(api) { + admin.setup!({ + ...api, + capabilities: { + ...api.capabilities, + provide: (name: string, factory: unknown) => { + const out = rewrite(name, factory); + if (out) { + (api.capabilities.provide as unknown as ( + n: string, + f: unknown, + ) => void)(out[0], out[1]); + } + }, + } as never, + }); + }, +}); + +Deno.test("conformance catches an admin that provides no admin.prefix", async () => { + const noPrefix = rewriteProvides((name, factory) => + name === "admin.prefix" ? null : [name, factory] + ); + await assertRejects( + () => + find(driverWith("no-prefix", noPrefix), "provides admin.prefix").run(), + Error, + "exactly one admin.prefix provider", + ); +}); + +Deno.test("conformance catches an admin whose prefix core cannot reserve", async () => { + for (const bad of ["/stub-admin/", "stub-admin", "/"]) { + const broken = rewriteProvides((name, factory) => + name === "admin.prefix" + ? [name, () => ({ prefix: bad })] + : [name, factory] + ); + await assertRejects( + () => + find(driverWith("bad-prefix", broken), "provides admin.prefix").run(), + Error, + "must start with", + ); + } +}); + Deno.test("conformance catches an admin that declares a cookie its sign-in does not set", async () => { const wrongCookie = replaceSession(() => ({ sessionCookies: ["some_other_cookie"], From 3c3c5db800f29c59f950cdb673ca02eb0376f335 Mon Sep 17 00:00:00 2001 From: zumbrunn Date: Fri, 6 Nov 2026 23:34:26 +0100 Subject: [PATCH 2/6] chore: version 2.0.0 (the conformance driver lost adminPrefix) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 ++ deno.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c6f907..0147e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +Version 2.0.0 (the conformance driver changed incompatibly). + ### Changed — BREAKING - **The conformance driver has no `adminPrefix`.** The admin's URL prefix comes diff --git a/deno.json b/deno.json index c68de32..d22eaa1 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@dune/testing", - "version": "1.0.3", + "version": "2.0.0", "license": "MIT", "exports": { ".": "./src/mod.ts", From 7000678649a1f7758b5f8f7dd011db4a69fd15d1 Mon Sep 17 00:00:00 2001 From: zumbrunn Date: Sat, 7 Nov 2026 01:10:14 +0100 Subject: [PATCH 3/6] feat(harness)!: createTestHarness has no disableAdmin option Core no longer loads an admin by itself, so there is nothing to disable; a test that wants an admin lists it in plugins. Part of 2.0.0. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 3 +++ README.md | 13 ++++++------- src/app-harness.ts | 2 +- src/conformance.ts | 2 +- src/harness.ts | 14 ++------------ 5 files changed, 13 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0147e98..09b564a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ Version 2.0.0 (the conformance driver changed incompatibly). provider, a usable prefix), core reserves that prefix against plugin routes, and with no admin `/admin` is an ordinary plugin path. The stub admin provides `admin.prefix` (`/stub-admin` by default). Needs `@dune/core` 0.36 (dune#123). +- **`createTestHarness({ disableAdmin })` is gone.** Core no longer loads an + admin by itself, so there is nothing to disable; pass an admin in `plugins` to + test with one. ### Added diff --git a/README.md b/README.md index eb5ec1c..d089845 100644 --- a/README.md +++ b/README.md @@ -146,13 +146,12 @@ admin (`createStubAdmin()`) is the reference run. ### Options -| Option | Default | Description | -| -------------- | ------------------ | -------------------------------------------------------------------- | -| `content` | `{}` | Fixture content keyed by path relative to `content/` | -| `files` | `{}` | Extra files at arbitrary paths (config overrides, flex data, etc.) | -| `plugins` | `[]` | Plugin instances to register before bootstrap | -| `siteTitle` | `"Dune Test Site"` | Site title override | -| `disableAdmin` | `true` | Disable the admin plugin (heavy; skip unless testing admin behavior) | +| Option | Default | Description | +| ----------- | ------------------ | ------------------------------------------------------------------ | +| `content` | `{}` | Fixture content keyed by path relative to `content/` | +| `files` | `{}` | Extra files at arbitrary paths (config overrides, flex data, etc.) | +| `plugins` | `[]` | Plugin instances to register before bootstrap | +| `siteTitle` | `"Dune Test Site"` | Site title override | ## License diff --git a/src/app-harness.ts b/src/app-harness.ts index 1fc5be8..e25a7ee 100644 --- a/src/app-harness.ts +++ b/src/app-harness.ts @@ -40,7 +40,7 @@ export interface AppHarness extends TestHarness { /** * Options for {@link createAppHarness}: those of {@link createTestHarness}, with - * the built-in admin disabled unless `disableAdmin: false`. + * no admin unless the test passes one in `plugins`. */ export type AppHarnessOptions = TestHarnessOptions; diff --git a/src/conformance.ts b/src/conformance.ts index 56a0a68..c770cac 100644 --- a/src/conformance.ts +++ b/src/conformance.ts @@ -57,7 +57,7 @@ export interface ConformanceAppOptions { * is mount order, and the suite relies on the admin mounting after them. */ extraPlugins: DunePlugin[]; - /** False: build the app with no admin at all (the built-in one switched off too). */ + /** False: build the app with no admin at all. */ withAdmin: boolean; } diff --git a/src/harness.ts b/src/harness.ts index 866b681..7ebbe26 100644 --- a/src/harness.ts +++ b/src/harness.ts @@ -79,11 +79,6 @@ export interface TestHarnessOptions { * Site title override. Defaults to `"Dune Test Site"`. */ siteTitle?: string; - /** - * When true, disables the built-in admin plugin. - * Defaults to true (admin is heavy; disable unless testing admin behavior). - */ - disableAdmin?: boolean; } /** A fully bootstrapped in-memory Dune instance for testing. */ @@ -153,7 +148,6 @@ export async function createTestHarness( files = {}, plugins = [], siteTitle = "Dune Test Site", - disableAdmin = true, } = options; // ── 1. In-memory storage ──────────────────────────────────────────────────── @@ -189,12 +183,8 @@ export async function createTestHarness( plugins, // registered before any hooks fire buildSearch: true, dev: true, - // Disable admin by default — it tries to create filesystem directories - // (audit log, session store) that don't exist in the test environment. - configOverrides: disableAdmin - // deno-lint-ignore no-explicit-any - ? { admin: { enabled: false } as any } - : undefined, + // No admin: core loads only the plugins passed here, so a test that wants + // one lists it in `plugins`. }); // ── 3. In-process API handler ──────────────────────────────────────────────── From 72ce00370dca0ee37bfad3d0a5e449353c4834fd Mon Sep 17 00:00:00 2001 From: zumbrunn Date: Sat, 7 Nov 2026 16:21:08 +0100 Subject: [PATCH 4/6] fix: the stub admin grants on app:site Follows @dune/core's rename of the permission resource (duneorg/dune#130). Co-Authored-By: Claude Sonnet 5.5 --- src/stub-admin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stub-admin.ts b/src/stub-admin.ts index fe70b81..258ee9a 100644 --- a/src/stub-admin.ts +++ b/src/stub-admin.ts @@ -220,7 +220,7 @@ export function createStubAdmin(options: StubAdminOptions = {}): StubAdmin { who: { type: "user", id: who.user.id }, // deno-lint-ignore no-explicit-any canThey: "pages.update" as any, - onWhat: { type: "app", id: "admin" }, + onWhat: { type: "app", id: "site" }, }) : false; if (!allowed) return json({ error: "Forbidden" }, 403); From 695b1d81670afa6a73eb1a94986b406813aabfa2 Mon Sep 17 00:00:00 2001 From: zumbrunn Date: Sat, 7 Nov 2026 19:33:34 +0100 Subject: [PATCH 5/6] chore: use the editor.session and editor.inline slot names (duneorg/dune#125) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 4 ++-- README.md | 8 ++++---- src/conformance.ts | 22 +++++++++++----------- src/stub-admin.ts | 4 ++-- tests/conformance_negative_test.ts | 12 ++++++------ tests/stub_admin_test.ts | 10 +++++----- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b564a..32b7382 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ Version 2.0.0 (the conformance driver changed incompatibly). - **`runAdminConformance()`** (`@dune/testing/conformance`): the admin conformance suite. A driver says how to build an app with an admin installed and how to sign an editor in; the suite checks the contract with core (the - `auth.session` capability, session recognition and marker scrubbing, the + `editor.session` capability, session recognition and marker scrubbing, the declared session cookie, mount-order independence, `onPageUpdate` / `onPageCreate` / `onPageDelete` from the writer, `admin.pages` / `admin.routes` mounting, and the site serving with no admin). Also @@ -36,7 +36,7 @@ Version 2.0.0 (the conformance driver changed incompatibly). - **`createStubAdmin()`** (`@dune/testing/stub-admin`): a minimal replacement admin built only on core's injected services, capabilities and mount API. It proves Dune does not depend on `@dune/plugin-admin`: its tests cover login and - sessions, `auth.session` recognised by core's transform pipeline (and only its + sessions, `editor.session` recognised by core's transform pipeline (and only its own cookie), marker scrubbing, a page edit that fires `onPageUpdate`, a role without edit rights, and a site with no admin at all. Needs `@dune/core` 0.36 (duneorg/dune#97); version bump and the `^0.36` pin are release work. diff --git a/README.md b/README.md index d089845..6e4cbba 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Folder pages are served at their canonical path with a trailing slash A minimal replacement admin, built only on the services, capabilities and mount API core hands every plugin (it imports nothing from `@dune/plugin-admin`). It keeps users and sessions through `services.users` and `services.sessions`, -provides the `auth.session` and `scheduler.executor` capabilities, seeds authz +provides the `editor.session` and `scheduler.executor` capabilities, seeds authz tuples through `services.authz`, and offers its own `POST /stub-admin/login`, `GET /stub-admin/pages` and `POST /stub-admin/pages/edit` (which, as the writer, fires `onPageUpdate`). Use it to check that your plugin works with an admin that @@ -98,11 +98,11 @@ const h = await createAppHarness({ ### `runAdminConformance(driver)` What "meets the contract" means for an admin. Dune does not depend on -`@dune/plugin-admin`: any plugin that provides the `auth.session` capability is +`@dune/plugin-admin`: any plugin that provides the `editor.session` capability is the admin as far as core is concerned. Give the suite a driver (how to build an app with your admin, how to sign an editor in) and it registers these tests: -- the admin provides `auth.session` (exactly one provider, active); +- the admin provides `editor.session` (exactly one provider, active); - the public site serves, and anonymous visitors are not admins; - core's response pipeline recognises an editor session and **only** a real one: no cookie, an unrelated cookie and a forged session cookie get no auth and @@ -113,7 +113,7 @@ app with your admin, how to sign an editor in) and it registers these tests: shared cache); - an account without editing rights gets no editing chrome (`signInViewer`); - a route in a plugin mounted before the admin can still tell who the admin is - through `auth.session`; + through `editor.session`; - editing a page through the admin writes it and fires `onPageUpdate` exactly once (`editPage`); - creating a page fires `onPageCreate` exactly once and deleting one fires diff --git a/src/conformance.ts b/src/conformance.ts index c770cac..95f7f2d 100644 --- a/src/conformance.ts +++ b/src/conformance.ts @@ -3,7 +3,7 @@ * core, without knowing anything else about it. * * Dune does not depend on `@dune/plugin-admin`: any plugin that provides the - * `auth.session` capability is "the admin" as far as core is concerned. This + * `editor.session` capability is "the admin" as far as core is concerned. This * suite is what "meets the contract" means. Hand it a *driver* that says how to * build an app with your admin installed and how to sign an editor in; it * registers the tests. @@ -226,21 +226,21 @@ export function adminConformanceChecks( checks.push({ name: `conformance [${driver.name}]: ${name}`, run }); t( - "provides auth.session, the capability that makes it the admin", + "provides editor.session, the capability that makes it the admin", () => withApp(driver, {}, (app) => { const providers = app.bootstrap.capabilities.providers(); - const session = providers.find((p) => p.name === "auth.session"); - assertExists(session, "no plugin provides auth.session"); + const session = providers.find((p) => p.name === "editor.session"); + assertExists(session, "no plugin provides editor.session"); assertEquals( session.status, "active", - "the auth.session factory did not produce a provider", + "the editor.session factory did not produce a provider", ); assertEquals( - providers.filter((p) => p.name === "auth.session").length, + providers.filter((p) => p.name === "editor.session").length, 1, - "exactly one auth.session provider", + "exactly one editor.session provider", ); return Promise.resolve(); }), @@ -296,7 +296,7 @@ export function adminConformanceChecks( assertEquals(home.status, 200); assertStringIncludes(await home.text(), "Home"); - const auth = app.bootstrap.capabilities.get("auth.session")!; + const auth = app.bootstrap.capabilities.get("editor.session")!; const anonymous = await auth.authenticate( new Request("http://localhost:8000/"), ); @@ -358,7 +358,7 @@ export function adminConformanceChecks( "the cookie it declares is the cookie its sign-in sets", () => withApp(driver, {}, async (app) => { - const declared = app.bootstrap.capabilities.get("auth.session") + const declared = app.bootstrap.capabilities.get("editor.session") ?.sessionCookies; if (!declared) return; // undeclared is allowed: core then tries every request that has cookies const cookie = await driver.signInEditor(app); @@ -407,7 +407,7 @@ export function adminConformanceChecks( hooks: {}, mount({ app, capabilities }) { app.get("/__conformance/who", async (fc) => { - const result = await capabilities.get("auth.session")?.authenticate( + const result = await capabilities.get("editor.session")?.authenticate( fc.req, ); return new Response(result?.authenticated ? "admin" : "anonymous"); @@ -634,7 +634,7 @@ export function adminConformanceChecks( () => withApp(driver, { withAdmin: false }, async (app) => { assertEquals((await app.request("/")).status, 200); - assertEquals(app.bootstrap.capabilities.get("auth.session"), undefined); + assertEquals(app.bootstrap.capabilities.get("editor.session"), undefined); assertEquals(app.bootstrap.capabilities.get("admin.prefix"), undefined); assertEquals(app.bootstrap.authz, undefined); assertEquals((await app.request("/api/inline-edit/ws")).status, 501); diff --git a/src/stub-admin.ts b/src/stub-admin.ts index 258ee9a..772c51f 100644 --- a/src/stub-admin.ts +++ b/src/stub-admin.ts @@ -4,7 +4,7 @@ * * It implements just enough of an admin to exercise the plugin contract: it * authenticates users and keeps sessions through core's injected services, - * provides the `auth.session` and `scheduler.executor` capabilities, seeds + * provides the `editor.session` and `scheduler.executor` capabilities, seeds * authorisation tuples through `services.authz`, and offers a login, a page * listing and a page edit route of its own. It imports nothing from * `@dune/plugin-admin`; everything it needs arrives through `PluginApi` and @@ -147,7 +147,7 @@ export function createStubAdmin(options: StubAdminOptions = {}): StubAdmin { state = { users, sessions: null, passwords: services.passwords, ready }; capabilities.provide("admin.prefix", () => ({ prefix })); - capabilities.provide("auth.session", () => ({ + capabilities.provide("editor.session", () => ({ ...(declareCookie ? { sessionCookies: [cookieName] } : {}), authenticate, })); diff --git a/tests/conformance_negative_test.ts b/tests/conformance_negative_test.ts index 3431719..a8cdab5 100644 --- a/tests/conformance_negative_test.ts +++ b/tests/conformance_negative_test.ts @@ -74,7 +74,7 @@ const find = (driver: AdminConformanceDriver, fragment: string) => { return check; }; -/** Run the stub's own setup, then replace `auth.session` with `override`. */ +/** Run the stub's own setup, then replace `editor.session` with `override`. */ const replaceSession = (override: (cookieName: string) => unknown): Breaker => (admin, { cookieName }) => ({ @@ -82,16 +82,16 @@ const replaceSession = setup(api) { admin.setup!(api); api.capabilities.provide( - "auth.session", + "editor.session", () => override(cookieName) as never, ); }, }); -Deno.test("conformance catches an admin that provides no auth.session", async () => { +Deno.test("conformance catches an admin that provides no editor.session", async () => { const bare: Breaker = (admin) => ({ ...admin, setup: () => {} }); await assertRejects(() => - find(driverWith("no-session", bare), "provides auth.session").run() + find(driverWith("no-session", bare), "provides editor.session").run() ); }); @@ -177,7 +177,7 @@ Deno.test("conformance catches an admin that accepts a forged session as the rea capabilities: { ...api.capabilities, provide: (name: string, factory: unknown) => { - if (name === "auth.session") { + if (name === "editor.session") { real = (factory as (ctx: unknown) => typeof real)({}); } else { (api.capabilities.provide as unknown as ( @@ -189,7 +189,7 @@ Deno.test("conformance catches an admin that accepts a forged session as the rea } as never, }); let last: unknown; - api.capabilities.provide("auth.session", () => ({ + api.capabilities.provide("editor.session", () => ({ authenticate: async (req: Request) => { const r = await real.authenticate(req); if (r.authenticated) { diff --git a/tests/stub_admin_test.ts b/tests/stub_admin_test.ts index d74b089..6c4d4f4 100644 --- a/tests/stub_admin_test.ts +++ b/tests/stub_admin_test.ts @@ -74,12 +74,12 @@ Deno.test("stub admin: the public site serves with it installed, and core sees w const providers = Object.fromEntries( h.bootstrap.capabilities.providers().map((p) => [p.name, p]), ); - assertEquals(providers["auth.session"].provider, "stub-admin"); - assertEquals(providers["auth.session"].status, "active"); + assertEquals(providers["editor.session"].provider, "stub-admin"); + assertEquals(providers["editor.session"].status, "active"); assertEquals(providers["scheduler.executor"].status, "active"); assertExists( h.bootstrap.authz, - "core creates authz for any auth.session provider, with admin.enabled false", + "core creates authz for any editor.session provider, with admin.enabled false", ); } finally { await h.dispose(); @@ -186,7 +186,7 @@ Deno.test("stub admin: a provider that does not declare its cookie is still foun }); try { assertEquals( - h.bootstrap.capabilities.get("auth.session")?.sessionCookies, + h.bootstrap.capabilities.get("editor.session")?.sessionCookies, undefined, ); const { cookie } = await login(h, admin.prefix, "ed", "pw"); @@ -275,7 +275,7 @@ Deno.test("without any admin the site still serves, and nothing admin-related ex const h = await createAppHarness({ content }); try { assertEquals((await h.request("/")).status, 200); - assertEquals(h.bootstrap.capabilities.get("auth.session"), undefined); + assertEquals(h.bootstrap.capabilities.get("editor.session"), undefined); assertEquals(h.bootstrap.authz, undefined); assertEquals((await h.request("/stub-admin/pages")).status, 404); assertEquals((await h.request("/api/inline-edit/ws")).status, 501); From 42ddd7cfef800f7d34c31f2a159b482bff3c5ce8 Mon Sep 17 00:00:00 2001 From: zumbrunn <chris@zumbrunn.com> Date: Tue, 10 Nov 2026 16:35:38 +0100 Subject: [PATCH 6/6] style: deno fmt (CHANGELOG, README, conformance) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- CHANGELOG.md | 8 ++++---- README.md | 6 +++--- src/conformance.ts | 12 ++++++++---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32b7382..80d416d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,10 +36,10 @@ Version 2.0.0 (the conformance driver changed incompatibly). - **`createStubAdmin()`** (`@dune/testing/stub-admin`): a minimal replacement admin built only on core's injected services, capabilities and mount API. It proves Dune does not depend on `@dune/plugin-admin`: its tests cover login and - sessions, `editor.session` recognised by core's transform pipeline (and only its - own cookie), marker scrubbing, a page edit that fires `onPageUpdate`, a role - without edit rights, and a site with no admin at all. Needs `@dune/core` 0.36 - (duneorg/dune#97); version bump and the `^0.36` pin are release work. + sessions, `editor.session` recognised by core's transform pipeline (and only + its own cookie), marker scrubbing, a page edit that fires `onPageUpdate`, a + role without edit rights, and a site with no admin at all. Needs `@dune/core` + 0.36 (duneorg/dune#97); version bump and the `^0.36` pin are release work. ## [1.0.3] — 2026-10-28 diff --git a/README.md b/README.md index 6e4cbba..c27578a 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,9 @@ const h = await createAppHarness({ ### `runAdminConformance(driver)` What "meets the contract" means for an admin. Dune does not depend on -`@dune/plugin-admin`: any plugin that provides the `editor.session` capability is -the admin as far as core is concerned. Give the suite a driver (how to build an -app with your admin, how to sign an editor in) and it registers these tests: +`@dune/plugin-admin`: any plugin that provides the `editor.session` capability +is the admin as far as core is concerned. Give the suite a driver (how to build +an app with your admin, how to sign an editor in) and it registers these tests: - the admin provides `editor.session` (exactly one provider, active); - the public site serves, and anonymous visitors are not admins; diff --git a/src/conformance.ts b/src/conformance.ts index 95f7f2d..1c25b16 100644 --- a/src/conformance.ts +++ b/src/conformance.ts @@ -407,9 +407,10 @@ export function adminConformanceChecks( hooks: {}, mount({ app, capabilities }) { app.get("/__conformance/who", async (fc) => { - const result = await capabilities.get("editor.session")?.authenticate( - fc.req, - ); + const result = await capabilities.get("editor.session") + ?.authenticate( + fc.req, + ); return new Response(result?.authenticated ? "admin" : "anonymous"); }); }, @@ -634,7 +635,10 @@ export function adminConformanceChecks( () => withApp(driver, { withAdmin: false }, async (app) => { assertEquals((await app.request("/")).status, 200); - assertEquals(app.bootstrap.capabilities.get("editor.session"), undefined); + assertEquals( + app.bootstrap.capabilities.get("editor.session"), + undefined, + ); assertEquals(app.bootstrap.capabilities.get("admin.prefix"), undefined); assertEquals(app.bootstrap.authz, undefined); assertEquals((await app.request("/api/inline-edit/ws")).status, 501);