Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,37 @@
All notable changes to @dune/plugin-admin are documented here. This project
follows [Semantic Versioning](https://semver.org).

## [Unreleased]

### Changed

- **Public forms and the incoming webhook moved to `@dune/core` (duneorg/dune#78).**
`POST /api/forms/:name`, `GET /api/forms/:name`, `POST /api/contact` and
`POST /api/webhook/incoming` are now served by core, so they work with this
plugin off. This plugin no longer registers them and no longer sends the
submission email or webhook (`public-api.ts`, `email.ts` and `webhook.ts` are
removed). The submissions UI is unchanged; it reads the same files through
`BootstrapResult.submissions`, and `createSubmissionManager` and the
submission types are re-exported from `@dune/core/forms`. **Requires a
`@dune/core` that provides `BootstrapResult.submissions`**; with an older
core the plugin stops at startup with a message saying so.
- **Incoming-webhook actions are still audited.** Core fires `onSystemAction`
and this plugin writes the `system.rebuild` / `system.cache_purge` audit
entries from it.
- **`mountDuneAdmin()` still gives a headless app the public form routes**,
by calling core's `registerFormRoutes()` (a no-op when `createDuneApp()` has
registered them).
- **Requires `@dune/core` 0.35.2 or newer.** The `@dune/core` range moves from
`0.35` to `^0.35.2`, the first core that serves the public form routes.
- **The data directory follows `system.data_dir`**, falling back to the
deprecated `admin.dataDir`, the same as core.

### Security

- **`dev/apply` refuses to set `system.webhooks`.** Incoming-webhook tokens
moved there from `admin.incoming_webhooks`, which the endpoint already
blocked; the new location is blocked too.

---

## [3.1.3] — 2026-10-27
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"./admin/auth/provider": "./src/admin/auth/provider.ts"
},
"imports": {
"@dune/core": "jsr:@dune/core@0.35",
"@dune/core": "jsr:@dune/core@^0.35.2",
"fresh": "jsr:@fresh/core@^2",
"preact": "npm:preact@^10",
"preact/hooks": "npm:preact@^10/hooks",
Expand Down
32 changes: 27 additions & 5 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { createWorkflowEngine } from "@dune/core/workflow";
import { createScheduler } from "@dune/core/workflow";
import type { ScheduledAction } from "@dune/core/workflow";
import { applyWorkflowTransition, SCHEDULED_ACTION_STATUS } from "./src/admin/workflow-actions.ts";
import { createSubmissionManager } from "./src/admin/submissions.ts";
import { resolveFormsConfig } from "@dune/core/forms";
import { createStagingEngine } from "@dune/core/staging";
import { createCommentManager } from "./src/admin/comments.ts";
import { createCollabManager } from "./src/collab/mod.ts";
Expand Down Expand Up @@ -177,7 +177,9 @@ export function createAdminPlugin(
enabled: true,
};
const runtimeDir = adminCfg.runtimeDir ?? ".dune/admin";
const dataDir = adminCfg.dataDir ?? "data";
// `system.data_dir`, falling back to the deprecated `admin.dataDir` —
// the same resolution core uses for the submissions it stores.
const dataDir = resolveFormsConfig(config).dataDir;

// ── Admin services ────────────────────────────────────────────────────────

Expand Down Expand Up @@ -293,9 +295,29 @@ export function createAdminPlugin(
contentDir: config.system.content.dir,
});

const submissionManager = createSubmissionManager({
storage,
submissionsDir: `${dataDir}/submissions`,
// Core accepts and stores public form submissions; the admin only
// manages them. An older core has no store to read.
const submissionManager = bootstrap.submissions;
if (!submissionManager) {
throw new Error(
"@dune/plugin-admin 3.2+ needs a @dune/core that stores form submissions itself " +
"(BootstrapResult.submissions). Upgrade @dune/core, or stay on @dune/plugin-admin 3.1.",
);
}

// The incoming webhook runs in core; keep the admin's audit trail.
bootstrap.hooks.on<
{ action: "rebuild" | "purge-cache"; ip: string | null; userAgent: string | null }
>("onSystemAction", ({ data }) => {
void setupState?.auditLogger?.log({
event: data.action === "rebuild" ? "system.rebuild" : "system.cache_purge",
actor: null,
ip: data.ip,
userAgent: data.userAgent,
target: { type: "system" },
detail: {},
outcome: "success",
}).catch(() => {});
});

// ── Default content editor ────────────────────────────────────────────────
Expand Down
20 changes: 11 additions & 9 deletions scripts/generate-core-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,18 @@
* auto-tracks patch releases within that minor — confirmed via `deno cache`:
* "0.28" resolved to the latest 0.28.x, not just 0.28.0.
*
* Bare minor again as of core 0.35: everything this package imports from core
* (`DunePlugin.mountEarly()` from 0.34.4, `./auth/authz-schema` from 0.34.2)
* exists from 0.35.0 on. If the package ever starts depending on something added
* at a *later* patch within a minor, re-floor to that patch (`^0.35.N`) — a bare
* "0.35" floors JSR's publish-time check at 0.35.0 (reproduced 2026-08-31 with
* "0.34": "invalid 'jsr:' dependency subpath: '@dune/core@0.34/auth/authz-schema',
* resolved to 0.34.0, has no export"). This constant and deno.json are only kept
* in step by `check:core-imports`, so run it before merging any pin change.
* Floored at 0.35.2: the public form routes and the submission store moved into
* core there (`BootstrapResult.submissions`, `registerFormRoutes()` and the
* `onSystemAction` hook from `@dune/core/forms`), and this package no longer
* registers them itself. A bare "0.35" would floor JSR's publish-time check at
* 0.35.0 (reproduced 2026-08-31 with "0.34":
* "invalid 'jsr:' dependency subpath: '@dune/core@0.34/auth/authz-schema',
* resolved to 0.34.0, has no export") and let a site pair this package with a
* core that cannot serve those routes. Back to a bare minor at the next core
* minor. This constant and deno.json are only kept in step by
* `check:core-imports`, so run it before merging any pin change.
*/
const CORE_RANGE = "0.35";
const CORE_RANGE = "^0.35.2";

const DENO_JSON = "deno.json";
const SCAN_ROOTS = ["mod.ts", "src", "tests"];
Expand Down
93 changes: 0 additions & 93 deletions src/admin/email.ts

This file was deleted.

32 changes: 6 additions & 26 deletions src/admin/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
* their own routes and want Dune only for content management.
*
* Instead of calling `createDuneApp()` (which owns all routes including `/*`),
* headless developers use `mountDuneAdmin()` to bolt on Dune's admin panel and
* public API, then wire their own `routes/` directory via `app.fsRoutes()`.
* headless developers use `mountDuneAdmin()` to bolt on Dune's admin panel, then wire their own `routes/` directory via `app.fsRoutes()`.
*
* @example
* ```ts
Expand Down Expand Up @@ -38,6 +37,7 @@
import type { App, FreshContext, Middleware } from "fresh";
import type { BootstrapResult } from "@dune/core/bootstrap";
import { registerPluginPublicRoutes } from "@dune/core/fresh-app";
import { registerFormRoutes } from "@dune/core/forms";
import { checkPermission } from "./routes/api/_utils.ts";
import type { AdminState } from "./types.ts";
import {
Expand All @@ -46,12 +46,6 @@ import {
adminMiddleware,
adminRoutes,
} from "./manifest.gen.ts";
import {
handleContactSubmission,
handleFormSchema,
handleFormSubmission,
handleIncomingWebhook,
} from "./public-api.ts";

/**
* `ctx` objects (`BootstrapResult`) for which the `ctx.state.adminContext`
Expand Down Expand Up @@ -244,24 +238,10 @@ export async function mountDuneAdmin(
registerPluginPublicRoutes(app, ctx, { adminPrefix });

// ── Public form / webhook API ───────────────────────────────────────────────
// Bind handlers to the per-site adminContext via closure so multisite
// deployments can't leak across tenants via a process-wide singleton.
if (adminContext) {
const adminCtx = adminContext;
app.post("/api/contact", (fc) => handleContactSubmission(adminCtx, fc.req));
app.get(
"/api/forms/:name",
(fc) => handleFormSchema(adminCtx, fc.params.name),
);
app.post(
"/api/forms/:name",
(fc) => handleFormSubmission(adminCtx, fc.req, fc.params.name),
);
app.post(
"/api/webhook/incoming",
(fc) => handleIncomingWebhook(adminCtx, fc.req),
);
}
// Served by core, with or without the admin panel. createDuneApp() already
// registers them, so this only matters for the headless path above, and it
// is a no-op when they are already there.
registerFormRoutes(app, ctx);
}

/**
Expand Down
Loading
Loading