diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9412e..f4e984f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,14 @@ follows [Semantic Versioning](https://semver.org). `services.authz.seedAdminTuples()` instead of importing `bootstrapAdminTuples`. Core builds them once per site under `admin.runtime_dir`; the admin no longer calls their factories. Requires `@dune/core` 0.36 or newer. +- **Blueprints and data paths come from core's `services`**: `services.blueprints` + (`has` / `resolve` / `validate`, read from the content engine at call time) + replaces importing `resolveBlueprint` / `validateFrontmatter`, and + `services.paths.dataDir` replaces `resolveDataDir()`. Requires `@dune/core` + 0.36 or newer. +- **`GET /admin/api/search/analytics` is removed.** It summarised + `search-analytics.jsonl`, which nothing in core or the first-party plugins + ever wrote, so it always returned an empty summary. - **Security policy, sessions, users and passwords come from core's `services`** (`MountApi.services`, `@dune/core/plugin-api`) instead of imports from `@dune/core`: HTML sanitising, SSRF-guarded fetch, body limits, client IP, the diff --git a/mod.ts b/mod.ts index bd5a754..14a73db 100644 --- a/mod.ts +++ b/mod.ts @@ -26,7 +26,6 @@ import { LocalAuthProvider } from "./src/admin/auth/local-provider.ts"; import type { AuthProvider } from "./src/admin/auth/provider.ts"; import type { ScheduledAction } from "@dune/core/workflow"; import { applyWorkflowTransition, SCHEDULED_ACTION_STATUS } from "./src/admin/workflow-actions.ts"; -import { resolveDataDir } from "@dune/core/forms"; import { createCommentManager } from "./src/admin/comments.ts"; import { createCollabManager } from "./src/collab/mod.ts"; import type { MachineTranslator } from "@dune/core/mt"; @@ -146,7 +145,7 @@ export function createAdminPlugin( }; const runtimeDir = adminCfg.runtime_dir ?? ".dune/admin"; // `system.data_dir` — the same resolution core uses for the submissions it stores. - const dataDir = resolveDataDir(config); + const dataDir = services.paths.dataDir; // ── Admin services ──────────────────────────────────────────────────────── diff --git a/src/admin/manifest.gen.ts b/src/admin/manifest.gen.ts index 5c81817..098eb79 100644 --- a/src/admin/manifest.gen.ts +++ b/src/admin/manifest.gen.ts @@ -51,53 +51,52 @@ import * as r44 from "./routes/api/preview.ts"; import * as r45 from "./routes/api/registry/plugins.ts"; import * as r46 from "./routes/api/registry/themes.ts"; import * as r47 from "./routes/api/render-markdown.ts"; -import * as r48 from "./routes/api/search/analytics.ts"; -import * as r49 from "./routes/api/search/engines.ts"; -import * as r50 from "./routes/api/sections.ts"; -import * as r51 from "./routes/api/staging/[...path]/index.ts"; -import * as r52 from "./routes/api/staging/[...path]/publish.ts"; -import * as r53 from "./routes/api/theme-preview.ts"; -import * as r54 from "./routes/api/themes/install.ts"; -import * as r55 from "./routes/api/users/index.ts"; -import * as r56 from "./routes/api/users/[id]/index.ts"; -import * as r57 from "./routes/api/users/[id]/password.ts"; -import * as r58 from "./routes/api/webhooks/deliveries.ts"; -import * as r59 from "./routes/api/workflow/schedule/index.ts"; -import * as r60 from "./routes/api/workflow/schedule/[id].ts"; -import * as r61 from "./routes/api/workflow/scheduled/[...path].ts"; -import * as r62 from "./routes/api/workflow/stages.ts"; -import * as r63 from "./routes/api/workflow/status/[...path].ts"; -import * as r64 from "./routes/api/workflow/transition.ts"; -import * as r65 from "./routes/audit/index.tsx"; -import * as r66 from "./routes/collab/edit-ws.ts"; -import * as r67 from "./routes/collab/ws.ts"; -import * as r68 from "./routes/config/index.tsx"; -import * as r69 from "./routes/email-preview/index.tsx"; -import * as r70 from "./routes/flex/index.tsx"; -import * as r71 from "./routes/flex/[type]/index.tsx"; -import * as r72 from "./routes/flex/[type]/[id].tsx"; -import * as r73 from "./routes/i18n/index.tsx"; -import * as r74 from "./routes/i18n/memory.tsx"; -import * as r75 from "./routes/jobs/index.tsx"; -import * as r76 from "./routes/login.tsx"; -import * as r77 from "./routes/login/logout.ts"; -import * as r78 from "./routes/marketplace/index.tsx"; -import * as r79 from "./routes/media/index.tsx"; -import * as r80 from "./routes/metrics/index.tsx"; -import * as r81 from "./routes/pages/index.tsx"; -import * as r82 from "./routes/pages/builder.tsx"; -import * as r83 from "./routes/pages/edit.tsx"; -import * as r84 from "./routes/pages/history.tsx"; -import * as r85 from "./routes/plugins/index.tsx"; -import * as r86 from "./routes/search/index.tsx"; -import * as r87 from "./routes/submissions/index.tsx"; -import * as r88 from "./routes/submissions/[form]/index.tsx"; -import * as r89 from "./routes/submissions/[form]/[id]/index.tsx"; -import * as r90 from "./routes/submissions/[form]/[id]/delete.ts"; -import * as r91 from "./routes/submissions/[form]/[id]/files/[filename].ts"; -import * as r92 from "./routes/submissions/[form]/[id]/status.ts"; -import * as r93 from "./routes/themes/index.tsx"; -import * as r94 from "./routes/users/index.tsx"; +import * as r48 from "./routes/api/search/engines.ts"; +import * as r49 from "./routes/api/sections.ts"; +import * as r50 from "./routes/api/staging/[...path]/index.ts"; +import * as r51 from "./routes/api/staging/[...path]/publish.ts"; +import * as r52 from "./routes/api/theme-preview.ts"; +import * as r53 from "./routes/api/themes/install.ts"; +import * as r54 from "./routes/api/users/index.ts"; +import * as r55 from "./routes/api/users/[id]/index.ts"; +import * as r56 from "./routes/api/users/[id]/password.ts"; +import * as r57 from "./routes/api/webhooks/deliveries.ts"; +import * as r58 from "./routes/api/workflow/schedule/index.ts"; +import * as r59 from "./routes/api/workflow/schedule/[id].ts"; +import * as r60 from "./routes/api/workflow/scheduled/[...path].ts"; +import * as r61 from "./routes/api/workflow/stages.ts"; +import * as r62 from "./routes/api/workflow/status/[...path].ts"; +import * as r63 from "./routes/api/workflow/transition.ts"; +import * as r64 from "./routes/audit/index.tsx"; +import * as r65 from "./routes/collab/edit-ws.ts"; +import * as r66 from "./routes/collab/ws.ts"; +import * as r67 from "./routes/config/index.tsx"; +import * as r68 from "./routes/email-preview/index.tsx"; +import * as r69 from "./routes/flex/index.tsx"; +import * as r70 from "./routes/flex/[type]/index.tsx"; +import * as r71 from "./routes/flex/[type]/[id].tsx"; +import * as r72 from "./routes/i18n/index.tsx"; +import * as r73 from "./routes/i18n/memory.tsx"; +import * as r74 from "./routes/jobs/index.tsx"; +import * as r75 from "./routes/login.tsx"; +import * as r76 from "./routes/login/logout.ts"; +import * as r77 from "./routes/marketplace/index.tsx"; +import * as r78 from "./routes/media/index.tsx"; +import * as r79 from "./routes/metrics/index.tsx"; +import * as r80 from "./routes/pages/index.tsx"; +import * as r81 from "./routes/pages/builder.tsx"; +import * as r82 from "./routes/pages/edit.tsx"; +import * as r83 from "./routes/pages/history.tsx"; +import * as r84 from "./routes/plugins/index.tsx"; +import * as r85 from "./routes/search/index.tsx"; +import * as r86 from "./routes/submissions/index.tsx"; +import * as r87 from "./routes/submissions/[form]/index.tsx"; +import * as r88 from "./routes/submissions/[form]/[id]/index.tsx"; +import * as r89 from "./routes/submissions/[form]/[id]/delete.ts"; +import * as r90 from "./routes/submissions/[form]/[id]/files/[filename].ts"; +import * as r91 from "./routes/submissions/[form]/[id]/status.ts"; +import * as r92 from "./routes/themes/index.tsx"; +import * as r93 from "./routes/users/index.tsx"; /** Shape of an fs-convention route module (subset Fresh cares about). */ export interface AdminRouteModule { @@ -165,53 +164,52 @@ export const adminRoutes: AdminRouteEntry[] = [ { pattern: "/api/registry/plugins", mod: r45 }, { pattern: "/api/registry/themes", mod: r46 }, { pattern: "/api/render-markdown", mod: r47 }, - { pattern: "/api/search/analytics", mod: r48 }, - { pattern: "/api/search/engines", mod: r49 }, - { pattern: "/api/sections", mod: r50 }, - { pattern: "/api/staging/:path*", mod: r51 }, - { pattern: "/api/staging/:path*/publish", mod: r52 }, - { pattern: "/api/theme-preview", mod: r53 }, - { pattern: "/api/themes/install", mod: r54 }, - { pattern: "/api/users", mod: r55 }, - { pattern: "/api/users/:id", mod: r56 }, - { pattern: "/api/users/:id/password", mod: r57 }, - { pattern: "/api/webhooks/deliveries", mod: r58 }, - { pattern: "/api/workflow/schedule", mod: r59 }, - { pattern: "/api/workflow/schedule/:id", mod: r60 }, - { pattern: "/api/workflow/scheduled/:path*", mod: r61 }, - { pattern: "/api/workflow/stages", mod: r62 }, - { pattern: "/api/workflow/status/:path*", mod: r63 }, - { pattern: "/api/workflow/transition", mod: r64 }, - { pattern: "/audit", mod: r65 }, - { pattern: "/collab/edit-ws", mod: r66 }, - { pattern: "/collab/ws", mod: r67 }, - { pattern: "/config", mod: r68 }, - { pattern: "/email-preview", mod: r69 }, - { pattern: "/flex", mod: r70 }, - { pattern: "/flex/:type", mod: r71 }, - { pattern: "/flex/:type/:id", mod: r72 }, - { pattern: "/i18n", mod: r73 }, - { pattern: "/i18n/memory", mod: r74 }, - { pattern: "/jobs", mod: r75 }, - { pattern: "/login", mod: r76 }, - { pattern: "/login/logout", mod: r77 }, - { pattern: "/marketplace", mod: r78 }, - { pattern: "/media", mod: r79 }, - { pattern: "/metrics", mod: r80 }, - { pattern: "/pages", mod: r81 }, - { pattern: "/pages/builder", mod: r82 }, - { pattern: "/pages/edit", mod: r83 }, - { pattern: "/pages/history", mod: r84 }, - { pattern: "/plugins", mod: r85 }, - { pattern: "/search", mod: r86 }, - { pattern: "/submissions", mod: r87 }, - { pattern: "/submissions/:form", mod: r88 }, - { pattern: "/submissions/:form/:id", mod: r89 }, - { pattern: "/submissions/:form/:id/delete", mod: r90 }, - { pattern: "/submissions/:form/:id/files/:filename", mod: r91 }, - { pattern: "/submissions/:form/:id/status", mod: r92 }, - { pattern: "/themes", mod: r93 }, - { pattern: "/users", mod: r94 }, + { pattern: "/api/search/engines", mod: r48 }, + { pattern: "/api/sections", mod: r49 }, + { pattern: "/api/staging/:path*", mod: r50 }, + { pattern: "/api/staging/:path*/publish", mod: r51 }, + { pattern: "/api/theme-preview", mod: r52 }, + { pattern: "/api/themes/install", mod: r53 }, + { pattern: "/api/users", mod: r54 }, + { pattern: "/api/users/:id", mod: r55 }, + { pattern: "/api/users/:id/password", mod: r56 }, + { pattern: "/api/webhooks/deliveries", mod: r57 }, + { pattern: "/api/workflow/schedule", mod: r58 }, + { pattern: "/api/workflow/schedule/:id", mod: r59 }, + { pattern: "/api/workflow/scheduled/:path*", mod: r60 }, + { pattern: "/api/workflow/stages", mod: r61 }, + { pattern: "/api/workflow/status/:path*", mod: r62 }, + { pattern: "/api/workflow/transition", mod: r63 }, + { pattern: "/audit", mod: r64 }, + { pattern: "/collab/edit-ws", mod: r65 }, + { pattern: "/collab/ws", mod: r66 }, + { pattern: "/config", mod: r67 }, + { pattern: "/email-preview", mod: r68 }, + { pattern: "/flex", mod: r69 }, + { pattern: "/flex/:type", mod: r70 }, + { pattern: "/flex/:type/:id", mod: r71 }, + { pattern: "/i18n", mod: r72 }, + { pattern: "/i18n/memory", mod: r73 }, + { pattern: "/jobs", mod: r74 }, + { pattern: "/login", mod: r75 }, + { pattern: "/login/logout", mod: r76 }, + { pattern: "/marketplace", mod: r77 }, + { pattern: "/media", mod: r78 }, + { pattern: "/metrics", mod: r79 }, + { pattern: "/pages", mod: r80 }, + { pattern: "/pages/builder", mod: r81 }, + { pattern: "/pages/edit", mod: r82 }, + { pattern: "/pages/history", mod: r83 }, + { pattern: "/plugins", mod: r84 }, + { pattern: "/search", mod: r85 }, + { pattern: "/submissions", mod: r86 }, + { pattern: "/submissions/:form", mod: r87 }, + { pattern: "/submissions/:form/:id", mod: r88 }, + { pattern: "/submissions/:form/:id/delete", mod: r89 }, + { pattern: "/submissions/:form/:id/files/:filename", mod: r90 }, + { pattern: "/submissions/:form/:id/status", mod: r91 }, + { pattern: "/themes", mod: r92 }, + { pattern: "/users", mod: r93 }, ]; /** Island files under src/admin/islands/, for Builder.registerIsland(). */ diff --git a/src/admin/routes/api/config/theme-config.ts b/src/admin/routes/api/config/theme-config.ts index c2a3cc7..6902ec0 100644 --- a/src/admin/routes/api/config/theme-config.ts +++ b/src/admin/routes/api/config/theme-config.ts @@ -1,6 +1,5 @@ /** GET + PUT /admin/api/config/theme-config */ -import { resolveDataDir } from "@dune/core/forms"; import type { AdminState } from "../../../types.ts"; import { requirePermission, json, serverError, csrfCheck } from "../_utils.ts"; import type { BlueprintField } from "@dune/core/blueprints"; @@ -25,7 +24,7 @@ export const handler = { const denied = await requirePermission(ctx, "config.update"); if (denied) return denied; - const { engine, storage, config, hooks } = ctx.state.adminContext; + const { engine, storage, hooks } = ctx.state.adminContext; try { const body = await ctx.req.json() as Record; const manifest = engine.themes.theme.manifest; @@ -59,7 +58,7 @@ export const handler = { } } - const dataDir = resolveDataDir(config); + const dataDir = ctx.state.adminContext.services.paths.dataDir; const themeConfigPath = `${dataDir}/theme-config.json`; const themeName = engine.config.theme.name; diff --git a/src/admin/routes/api/pages/[...path]/index.ts b/src/admin/routes/api/pages/[...path]/index.ts index 80311c3..638de1c 100644 --- a/src/admin/routes/api/pages/[...path]/index.ts +++ b/src/admin/routes/api/pages/[...path]/index.ts @@ -12,7 +12,6 @@ import type { AdminState } from "../../../../types.ts"; import { requirePermission, requireTsxWrite, requirePageOwner, requireOwnedPage, pageOwnerId, json, serverError, actorFromAuth, getClientIp, csrfCheck, validatePagePath } from "../../_utils.ts"; import { stringify as stringifyYaml } from "@std/yaml"; -import { resolveBlueprint, validateFrontmatter } from "@dune/core/blueprints"; import { fireContentWebhooks } from "../../../../../admin/webhooks.ts"; import type { PageFrontmatter } from "@dune/core/content/types"; import type { FreshContext } from "fresh"; @@ -51,10 +50,7 @@ export const handler = { // nothing for every site with blueprints configured. Resolve // inheritance (`extends`) the same way PUT's validateFrontmatter() // does, rather than sending the raw unresolved definition. - const rawBlueprint = engine.blueprints[page.template]; - const blueprint = rawBlueprint - ? resolveBlueprint(page.template, rawBlueprint, engine.blueprints, 0) - : null; + const blueprint = ctx.state.adminContext.services.blueprints.resolve(page.template); return json({ sourcePath: page.sourcePath, route: page.route, format: page.format, template: page.template, frontmatter: page.frontmatter, rawContent: page.rawContent, @@ -113,8 +109,9 @@ export const handler = { else delete mergedFm.createdBy; const template = (mergedFm.template as string) ?? page.template; - if (engine.blueprints[template]) { - const errors = validateFrontmatter(mergedFm as PageFrontmatter, template, engine.blueprints); + const { blueprints } = ctx.state.adminContext.services; + if (blueprints.has(template)) { + const errors = blueprints.validate(mergedFm as PageFrontmatter, template); if (errors.length > 0) { return json({ error: "Blueprint validation failed", validationErrors: errors }, 422); } diff --git a/src/admin/routes/api/plugins/[name]/config.ts b/src/admin/routes/api/plugins/[name]/config.ts index b254943..6b83b1a 100644 --- a/src/admin/routes/api/plugins/[name]/config.ts +++ b/src/admin/routes/api/plugins/[name]/config.ts @@ -1,6 +1,5 @@ /** PUT /admin/api/plugins/:name/config */ -import { resolveDataDir } from "@dune/core/forms"; import type { AdminState } from "../../../../types.ts"; import { requirePermission, json, serverError, csrfCheck } from "../../_utils.ts"; import type { BlueprintField } from "@dune/core/blueprints"; @@ -75,7 +74,7 @@ export const handler = { if (errors.length > 0) return json({ error: `Missing required fields: ${errors.join(", ")}` }, 422); } - const dataDir = resolveDataDir(config); + const dataDir = ctx.state.adminContext.services.paths.dataDir; const filePath = `${dataDir}/plugins/${pluginName}.json`; await storage.write(filePath, new TextEncoder().encode(JSON.stringify(body, null, 2))); diff --git a/src/admin/routes/api/search/analytics.ts b/src/admin/routes/api/search/analytics.ts deleted file mode 100644 index a3c7231..0000000 --- a/src/admin/routes/api/search/analytics.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** GET /admin/api/search/analytics */ - -import type { AdminState } from "../../../types.ts"; -import { requirePermission, json, serverError } from "../_utils.ts"; -import { createSearchAnalytics } from "@dune/core/search"; -import { join } from "@std/path"; -import type { FreshContext } from "fresh"; - -export const handler = { - async GET(ctx: FreshContext) { - const denied = await requirePermission(ctx, "pages.read"); - if (denied) return denied; - const { config } = ctx.state.adminContext; - try { - const runtimeDir = config.admin?.runtime_dir ?? ".dune/admin"; - const analyticsPath = join(runtimeDir, "search-analytics.jsonl"); - const summary = await createSearchAnalytics(analyticsPath).summarize(); - return json(summary); - } catch (err) { - return serverError(err); - } - }, -}; diff --git a/tests/admin/pages_edit_blueprint_test.ts b/tests/admin/pages_edit_blueprint_test.ts index 4dbbb50..d32ceda 100644 --- a/tests/admin/pages_edit_blueprint_test.ts +++ b/tests/admin/pages_edit_blueprint_test.ts @@ -61,7 +61,7 @@ function makeCtx(opts: { state: { auth: { authenticated: true, user: { id: "1", username: "admin", roles: ["admin"] } }, adminContext: { - services: makeServices(null), + services: makeServices(null, { getBlueprints: () => engine.blueprints as never }), engine, storage: opts.storage, config: { system: { content: { dir: "content" } }, admin: {} }, diff --git a/tests/admin/theme_config_test.ts b/tests/admin/theme_config_test.ts index 0b3334c..5b206a2 100644 --- a/tests/admin/theme_config_test.ts +++ b/tests/admin/theme_config_test.ts @@ -7,6 +7,7 @@ import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"; import { handler } from "../../src/admin/routes/api/config/theme-config.ts"; +import { makeServices } from "../helpers/services.ts"; // deno-lint-ignore no-explicit-any function makeStorage(initial: Record = {}): any { @@ -48,6 +49,7 @@ function makeCtx(opts: { state: { auth: { authenticated: true, user: { id: "1", username: "admin" } }, adminContext: { + services: makeServices(opts.storage), engine, storage: opts.storage, config: { admin: { data_dir: "data" } }, diff --git a/tests/helpers/services.ts b/tests/helpers/services.ts index b58c457..03f4408 100644 --- a/tests/helpers/services.ts +++ b/tests/helpers/services.ts @@ -1,12 +1,13 @@ import { createPluginServices } from "@dune/core/plugin-api"; -import type { PluginServices } from "@dune/core/plugin-api"; +import type { PluginServices, PluginServicesOptions } from "@dune/core/plugin-api"; import type { DuneConfig } from "@dune/core/config"; import type { StorageAdapter } from "@dune/core/storage"; /** Core's plugin services over a test storage (default config: local sessions). */ -export function makeServices(storage: unknown): PluginServices { +export function makeServices(storage: unknown, extra: Partial = {}): PluginServices { return createPluginServices({ config: { system: {}, site: {} } as unknown as DuneConfig, storage: storage as StorageAdapter, + ...extra, }); }