@plank-cms/plank 0.29.0 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/admin/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap"
|
|
13
13
|
rel="stylesheet"
|
|
14
14
|
/>
|
|
15
|
-
<script type="module" crossorigin src="/admin/assets/index-
|
|
15
|
+
<script type="module" crossorigin src="/admin/assets/index-RfZ7f9RV.js"></script>
|
|
16
16
|
<link rel="stylesheet" crossorigin href="/admin/assets/index-CHJ8C--M.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -93,7 +93,7 @@ function getUpdateScriptCommand(name) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// src/commands/init.ts
|
|
96
|
-
var PACKAGE_VERSION = "0.
|
|
96
|
+
var PACKAGE_VERSION = "0.30.1";
|
|
97
97
|
function generateSecret() {
|
|
98
98
|
return randomBytes(32).toString("hex");
|
|
99
99
|
}
|
|
@@ -203,7 +203,7 @@ import { dirname, join as join3, resolve as resolve2 } from "path";
|
|
|
203
203
|
async function start() {
|
|
204
204
|
config({ path: resolve2(process.cwd(), ".env") });
|
|
205
205
|
process.env.PLANK_ADMIN_DIST = join3(dirname(fileURLToPath(import.meta.url)), "admin");
|
|
206
|
-
const { start: startServer } = await import("./server-
|
|
206
|
+
const { start: startServer } = await import("./server-BDQYF2ZA.js");
|
|
207
207
|
await startServer();
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -4528,7 +4528,7 @@ import { randomBytes as randomBytes7, createHash } from "crypto";
|
|
|
4528
4528
|
import { z as z7, flattenError as flattenError5 } from "zod";
|
|
4529
4529
|
var CreateTokenSchema = z7.object({
|
|
4530
4530
|
name: z7.string().min(1),
|
|
4531
|
-
accessType: z7.enum(["read-only", "full-access"])
|
|
4531
|
+
accessType: z7.enum(["read-only", "full-access", "mcp-server"])
|
|
4532
4532
|
});
|
|
4533
4533
|
function hashToken(token) {
|
|
4534
4534
|
return createHash("sha256").update(token).digest("hex");
|
|
@@ -5834,7 +5834,9 @@ import { Router as Router3 } from "express";
|
|
|
5834
5834
|
// ../core/dist/middlewares/apiToken.js
|
|
5835
5835
|
import { createHash as createHash2 } from "crypto";
|
|
5836
5836
|
var READ_ONLY_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
5837
|
-
|
|
5837
|
+
var PUBLIC_API_ACCESS_TYPES = /* @__PURE__ */ new Set(["read-only", "full-access"]);
|
|
5838
|
+
var MCP_ACCESS_TYPES = /* @__PURE__ */ new Set(["mcp-server"]);
|
|
5839
|
+
async function enforceApiToken(req, res, next, allowedAccessTypes) {
|
|
5838
5840
|
const header = req.headers.authorization;
|
|
5839
5841
|
if (!header?.startsWith("Bearer ")) {
|
|
5840
5842
|
res.status(401).json({ error: "API token required" });
|
|
@@ -5847,12 +5849,26 @@ async function apiToken(req, res, next) {
|
|
|
5847
5849
|
res.status(401).json({ error: "Invalid API token" });
|
|
5848
5850
|
return;
|
|
5849
5851
|
}
|
|
5852
|
+
if (!allowedAccessTypes.has(rows[0].access_type)) {
|
|
5853
|
+
res.status(403).json({ error: "This token cannot access this endpoint" });
|
|
5854
|
+
return;
|
|
5855
|
+
}
|
|
5850
5856
|
if (rows[0].access_type === "read-only" && !READ_ONLY_METHODS.has(req.method)) {
|
|
5851
5857
|
res.status(403).json({ error: "This token only allows read access" });
|
|
5852
5858
|
return;
|
|
5853
5859
|
}
|
|
5860
|
+
req.apiToken = {
|
|
5861
|
+
id: rows[0].id,
|
|
5862
|
+
accessType: rows[0].access_type
|
|
5863
|
+
};
|
|
5854
5864
|
next();
|
|
5855
5865
|
}
|
|
5866
|
+
async function apiToken(req, res, next) {
|
|
5867
|
+
await enforceApiToken(req, res, next, PUBLIC_API_ACCESS_TYPES);
|
|
5868
|
+
}
|
|
5869
|
+
async function mcpToken(req, res, next) {
|
|
5870
|
+
await enforceApiToken(req, res, next, MCP_ACCESS_TYPES);
|
|
5871
|
+
}
|
|
5856
5872
|
|
|
5857
5873
|
// ../core/dist/controllers/public.js
|
|
5858
5874
|
function createMediaValue(url, options) {
|
|
@@ -6453,6 +6469,276 @@ router3.get("/:slug", listPublicEntries);
|
|
|
6453
6469
|
router3.get("/:slug/:id", getPublicEntry);
|
|
6454
6470
|
var public_default = router3;
|
|
6455
6471
|
|
|
6472
|
+
// ../core/dist/routes/mcp.js
|
|
6473
|
+
import { Router as Router4 } from "express";
|
|
6474
|
+
|
|
6475
|
+
// ../core/dist/controllers/mcp.js
|
|
6476
|
+
var MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
6477
|
+
var JSON_RPC_VERSION = "2.0";
|
|
6478
|
+
var CONTENT_TYPES_URI = "plank://content-types";
|
|
6479
|
+
var LOCALES_URI = "plank://locales";
|
|
6480
|
+
function buildSchemaUri(slug) {
|
|
6481
|
+
return `plank://content-types/${slug}/schema`;
|
|
6482
|
+
}
|
|
6483
|
+
function parseLocales(raw, fallback) {
|
|
6484
|
+
if (!raw)
|
|
6485
|
+
return [fallback];
|
|
6486
|
+
try {
|
|
6487
|
+
const parsed = JSON.parse(raw);
|
|
6488
|
+
if (!Array.isArray(parsed))
|
|
6489
|
+
return [fallback];
|
|
6490
|
+
const locales = parsed.filter((value) => typeof value === "string" && value.length > 0);
|
|
6491
|
+
return locales.length > 0 ? [...new Set(locales)] : [fallback];
|
|
6492
|
+
} catch {
|
|
6493
|
+
return [fallback];
|
|
6494
|
+
}
|
|
6495
|
+
}
|
|
6496
|
+
async function getLocalesPayload() {
|
|
6497
|
+
const settings = await getSettings("general");
|
|
6498
|
+
const defaultLocale = settings.default_locale ?? "en";
|
|
6499
|
+
const locales = parseLocales(settings.locales, defaultLocale);
|
|
6500
|
+
if (!locales.includes(defaultLocale)) {
|
|
6501
|
+
locales.unshift(defaultLocale);
|
|
6502
|
+
}
|
|
6503
|
+
return { defaultLocale, locales };
|
|
6504
|
+
}
|
|
6505
|
+
async function getContentTypeSummaries() {
|
|
6506
|
+
const contentTypes = await findAllContentTypes();
|
|
6507
|
+
return contentTypes.map((contentType) => ({
|
|
6508
|
+
name: contentType.name,
|
|
6509
|
+
slug: contentType.slug,
|
|
6510
|
+
kind: contentType.kind,
|
|
6511
|
+
previewEnabled: contentType.previewEnabled ?? true,
|
|
6512
|
+
isDefault: contentType.isDefault ?? false,
|
|
6513
|
+
schemaUri: buildSchemaUri(contentType.slug),
|
|
6514
|
+
updatedAt: contentType.updatedAt?.toISOString() ?? null
|
|
6515
|
+
}));
|
|
6516
|
+
}
|
|
6517
|
+
async function listResources() {
|
|
6518
|
+
const contentTypes = await getContentTypeSummaries();
|
|
6519
|
+
return [
|
|
6520
|
+
{
|
|
6521
|
+
name: "content-types",
|
|
6522
|
+
title: "Content Types",
|
|
6523
|
+
uri: CONTENT_TYPES_URI,
|
|
6524
|
+
description: "Lists the content types available in this Plank instance.",
|
|
6525
|
+
mimeType: "application/json"
|
|
6526
|
+
},
|
|
6527
|
+
{
|
|
6528
|
+
name: "locales",
|
|
6529
|
+
title: "Locales",
|
|
6530
|
+
uri: LOCALES_URI,
|
|
6531
|
+
description: "Lists the enabled locales and the default locale for this Plank instance.",
|
|
6532
|
+
mimeType: "application/json"
|
|
6533
|
+
},
|
|
6534
|
+
...contentTypes.map((contentType) => ({
|
|
6535
|
+
name: `content-type-schema-${contentType.slug}`,
|
|
6536
|
+
title: `${contentType.name} schema`,
|
|
6537
|
+
uri: contentType.schemaUri,
|
|
6538
|
+
description: `Schema for the "${contentType.slug}" content type.`,
|
|
6539
|
+
mimeType: "application/json",
|
|
6540
|
+
annotations: contentType.updatedAt ? {
|
|
6541
|
+
lastModified: contentType.updatedAt
|
|
6542
|
+
} : void 0
|
|
6543
|
+
}))
|
|
6544
|
+
];
|
|
6545
|
+
}
|
|
6546
|
+
async function readResource(uri) {
|
|
6547
|
+
if (uri === CONTENT_TYPES_URI) {
|
|
6548
|
+
return {
|
|
6549
|
+
contents: [
|
|
6550
|
+
{
|
|
6551
|
+
uri,
|
|
6552
|
+
mimeType: "application/json",
|
|
6553
|
+
text: JSON.stringify({ contentTypes: await getContentTypeSummaries() }, null, 2)
|
|
6554
|
+
}
|
|
6555
|
+
]
|
|
6556
|
+
};
|
|
6557
|
+
}
|
|
6558
|
+
if (uri === LOCALES_URI) {
|
|
6559
|
+
return {
|
|
6560
|
+
contents: [
|
|
6561
|
+
{
|
|
6562
|
+
uri,
|
|
6563
|
+
mimeType: "application/json",
|
|
6564
|
+
text: JSON.stringify(await getLocalesPayload(), null, 2)
|
|
6565
|
+
}
|
|
6566
|
+
]
|
|
6567
|
+
};
|
|
6568
|
+
}
|
|
6569
|
+
const match = /^plank:\/\/content-types\/([^/]+)\/schema$/.exec(uri);
|
|
6570
|
+
if (!match) {
|
|
6571
|
+
throw buildJsonRpcError(-32602, "Unknown resource URI", { uri });
|
|
6572
|
+
}
|
|
6573
|
+
const slug = decodeURIComponent(match[1] ?? "");
|
|
6574
|
+
const contentType = await findContentTypeBySlug(slug);
|
|
6575
|
+
if (!contentType) {
|
|
6576
|
+
throw buildJsonRpcError(-32004, "Content type not found", { slug });
|
|
6577
|
+
}
|
|
6578
|
+
return {
|
|
6579
|
+
contents: [
|
|
6580
|
+
{
|
|
6581
|
+
uri,
|
|
6582
|
+
mimeType: "application/json",
|
|
6583
|
+
text: JSON.stringify(contentType, null, 2)
|
|
6584
|
+
}
|
|
6585
|
+
]
|
|
6586
|
+
};
|
|
6587
|
+
}
|
|
6588
|
+
function buildJsonRpcError(code, message, data) {
|
|
6589
|
+
return data === void 0 ? { code, message } : { code, message, data };
|
|
6590
|
+
}
|
|
6591
|
+
function buildResult(id, result) {
|
|
6592
|
+
return {
|
|
6593
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
6594
|
+
id,
|
|
6595
|
+
result
|
|
6596
|
+
};
|
|
6597
|
+
}
|
|
6598
|
+
function buildError(id, error) {
|
|
6599
|
+
return {
|
|
6600
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
6601
|
+
id,
|
|
6602
|
+
error
|
|
6603
|
+
};
|
|
6604
|
+
}
|
|
6605
|
+
function isJsonRpcRequest(value) {
|
|
6606
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6607
|
+
}
|
|
6608
|
+
async function handleRequest(message) {
|
|
6609
|
+
if (message.jsonrpc !== JSON_RPC_VERSION) {
|
|
6610
|
+
return buildError(message.id ?? null, buildJsonRpcError(-32600, "Invalid JSON-RPC version"));
|
|
6611
|
+
}
|
|
6612
|
+
if (typeof message.method !== "string" || message.method.length === 0) {
|
|
6613
|
+
return buildError(message.id ?? null, buildJsonRpcError(-32600, "Invalid method"));
|
|
6614
|
+
}
|
|
6615
|
+
if (message.id === void 0) {
|
|
6616
|
+
if (message.method === "notifications/initialized") {
|
|
6617
|
+
return null;
|
|
6618
|
+
}
|
|
6619
|
+
return null;
|
|
6620
|
+
}
|
|
6621
|
+
try {
|
|
6622
|
+
switch (message.method) {
|
|
6623
|
+
case "initialize": {
|
|
6624
|
+
const version = await getCurrentVersion();
|
|
6625
|
+
return buildResult(message.id, {
|
|
6626
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
6627
|
+
capabilities: {
|
|
6628
|
+
resources: {}
|
|
6629
|
+
},
|
|
6630
|
+
serverInfo: {
|
|
6631
|
+
name: "plank-cms",
|
|
6632
|
+
title: "Plank CMS",
|
|
6633
|
+
version
|
|
6634
|
+
},
|
|
6635
|
+
instructions: "Use resources/list to discover available resources, then resources/read to load Plank content types, per-type schemas, and locales."
|
|
6636
|
+
});
|
|
6637
|
+
}
|
|
6638
|
+
case "ping":
|
|
6639
|
+
return buildResult(message.id, {});
|
|
6640
|
+
case "resources/list":
|
|
6641
|
+
return buildResult(message.id, { resources: await listResources() });
|
|
6642
|
+
case "resources/read": {
|
|
6643
|
+
const uri = message.params?.uri;
|
|
6644
|
+
if (typeof uri !== "string" || uri.length === 0) {
|
|
6645
|
+
return buildError(message.id, buildJsonRpcError(-32602, "A resource URI is required"));
|
|
6646
|
+
}
|
|
6647
|
+
return buildResult(message.id, await readResource(uri));
|
|
6648
|
+
}
|
|
6649
|
+
default:
|
|
6650
|
+
return buildError(message.id, buildJsonRpcError(-32601, "Method not found", { method: message.method }));
|
|
6651
|
+
}
|
|
6652
|
+
} catch (error) {
|
|
6653
|
+
if (isJsonRpcError(error)) {
|
|
6654
|
+
return buildError(message.id, error);
|
|
6655
|
+
}
|
|
6656
|
+
return buildError(message.id, buildJsonRpcError(-32603, "Internal server error"));
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
function isJsonRpcError(error) {
|
|
6660
|
+
return typeof error === "object" && error !== null && "code" in error && "message" in error;
|
|
6661
|
+
}
|
|
6662
|
+
async function handleMcpRequest(req, res) {
|
|
6663
|
+
const payload = req.body;
|
|
6664
|
+
if (Array.isArray(payload)) {
|
|
6665
|
+
if (payload.length === 0) {
|
|
6666
|
+
res.status(400).json(buildError(null, buildJsonRpcError(-32600, "Batch requests cannot be empty")));
|
|
6667
|
+
return;
|
|
6668
|
+
}
|
|
6669
|
+
const responses = (await Promise.all(payload.map(async (message) => {
|
|
6670
|
+
if (!isJsonRpcRequest(message)) {
|
|
6671
|
+
return buildError(null, buildJsonRpcError(-32600, "Invalid request"));
|
|
6672
|
+
}
|
|
6673
|
+
return await handleRequest(message);
|
|
6674
|
+
}))).filter((response2) => response2 !== null);
|
|
6675
|
+
if (responses.length === 0) {
|
|
6676
|
+
res.status(202).end();
|
|
6677
|
+
return;
|
|
6678
|
+
}
|
|
6679
|
+
res.json(responses);
|
|
6680
|
+
return;
|
|
6681
|
+
}
|
|
6682
|
+
if (!isJsonRpcRequest(payload)) {
|
|
6683
|
+
res.status(400).json(buildError(null, buildJsonRpcError(-32600, "Invalid request")));
|
|
6684
|
+
return;
|
|
6685
|
+
}
|
|
6686
|
+
const response = await handleRequest(payload);
|
|
6687
|
+
if (!response) {
|
|
6688
|
+
res.status(202).end();
|
|
6689
|
+
return;
|
|
6690
|
+
}
|
|
6691
|
+
res.json(response);
|
|
6692
|
+
}
|
|
6693
|
+
function handleMcpGet(_req, res) {
|
|
6694
|
+
res.status(200).json({
|
|
6695
|
+
name: "plank-cms",
|
|
6696
|
+
transport: "streamable-http",
|
|
6697
|
+
endpoint: "/mcp",
|
|
6698
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
6699
|
+
capabilities: ["resources"]
|
|
6700
|
+
});
|
|
6701
|
+
}
|
|
6702
|
+
function handleMcpDelete(_req, res) {
|
|
6703
|
+
res.status(405).json({ error: "Session termination is not supported" });
|
|
6704
|
+
}
|
|
6705
|
+
|
|
6706
|
+
// ../core/dist/middlewares/mcpOrigin.js
|
|
6707
|
+
function validateMcpOrigin(req, res, next) {
|
|
6708
|
+
const origin = req.get("origin");
|
|
6709
|
+
if (!origin) {
|
|
6710
|
+
next();
|
|
6711
|
+
return;
|
|
6712
|
+
}
|
|
6713
|
+
const host = req.get("host");
|
|
6714
|
+
if (!host) {
|
|
6715
|
+
res.status(400).json({ error: "Missing Host header" });
|
|
6716
|
+
return;
|
|
6717
|
+
}
|
|
6718
|
+
const forwardedProto = req.get("x-forwarded-proto");
|
|
6719
|
+
const protocol = forwardedProto ? forwardedProto.split(",")[0].trim() : req.protocol;
|
|
6720
|
+
const allowedOrigins = /* @__PURE__ */ new Set([`${protocol}://${host}`]);
|
|
6721
|
+
if (!process.env.PLANK_ADMIN_DIST) {
|
|
6722
|
+
const port = process.env.PLANK_PORT ?? "5500";
|
|
6723
|
+
allowedOrigins.add(`http://localhost:${port}`);
|
|
6724
|
+
allowedOrigins.add("http://localhost:3000");
|
|
6725
|
+
}
|
|
6726
|
+
if (!allowedOrigins.has(origin)) {
|
|
6727
|
+
res.status(403).json({ error: "Origin not allowed" });
|
|
6728
|
+
return;
|
|
6729
|
+
}
|
|
6730
|
+
next();
|
|
6731
|
+
}
|
|
6732
|
+
|
|
6733
|
+
// ../core/dist/routes/mcp.js
|
|
6734
|
+
var router4 = Router4();
|
|
6735
|
+
router4.use(validateMcpOrigin);
|
|
6736
|
+
router4.use(mcpToken);
|
|
6737
|
+
router4.get("/", handleMcpGet);
|
|
6738
|
+
router4.post("/", handleMcpRequest);
|
|
6739
|
+
router4.delete("/", handleMcpDelete);
|
|
6740
|
+
var mcp_default = router4;
|
|
6741
|
+
|
|
6456
6742
|
// ../core/dist/middlewares/errorHandler.js
|
|
6457
6743
|
import { ZodError, flattenError as flattenError6 } from "zod";
|
|
6458
6744
|
function parseUniqueViolationField(detail) {
|
|
@@ -6523,6 +6809,7 @@ var cmsCorOptions = cors({ origin: cmsAllowedOrigins, credentials: true });
|
|
|
6523
6809
|
app.use("/cms/auth", cmsCorOptions, auth_default);
|
|
6524
6810
|
app.use("/cms/admin", cmsCorOptions, admin_default);
|
|
6525
6811
|
app.use("/api", cors(), public_default);
|
|
6812
|
+
app.use("/mcp", mcp_default);
|
|
6526
6813
|
app.get("/", (_req, res) => {
|
|
6527
6814
|
if (isDev) {
|
|
6528
6815
|
res.redirect(adminDevUrl);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plank-cms/plank",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "Self-hosted headless CMS. Deploy in minutes on your own infrastructure.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/fs-extra": "^11.0.4",
|
|
57
57
|
"tsup": "^8.5.0",
|
|
58
|
-
"@plank-cms/core": "0.
|
|
59
|
-
"@plank-cms/
|
|
60
|
-
"@plank-cms/
|
|
58
|
+
"@plank-cms/core": "0.30.1",
|
|
59
|
+
"@plank-cms/db": "0.30.1",
|
|
60
|
+
"@plank-cms/schema": "0.30.1"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsup",
|