@plank-cms/plank 0.27.4 → 0.28.0
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/README.md +7 -0
- package/dist/admin/assets/index-BhxOFNX5.css +2 -0
- package/dist/admin/assets/{index-PC7ZVO5D.js → index-CdtvxQpy.js} +52 -52
- package/dist/admin/index.html +2 -2
- package/dist/index.js +2 -2
- package/dist/migrations/033_plank_addons.sql +22 -0
- package/dist/{server-BVXQ6LEC.js → server-DONLO5CM.js} +694 -11
- package/package.json +4 -4
- package/dist/admin/assets/index-BTElP7oS.css +0 -2
|
@@ -37,6 +37,8 @@ var DEFAULT_ROLE_PERMISSIONS = {
|
|
|
37
37
|
"media:read",
|
|
38
38
|
"media:write",
|
|
39
39
|
"media:delete",
|
|
40
|
+
"addons:read",
|
|
41
|
+
"addons:write",
|
|
40
42
|
"settings:overview:read",
|
|
41
43
|
"settings:users:read",
|
|
42
44
|
"settings:users:write",
|
|
@@ -650,8 +652,8 @@ function validate(contentType, payload) {
|
|
|
650
652
|
import express from "express";
|
|
651
653
|
import cors from "cors";
|
|
652
654
|
import helmet from "helmet";
|
|
653
|
-
import { join as
|
|
654
|
-
import { fileURLToPath as
|
|
655
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
656
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
655
657
|
|
|
656
658
|
// ../core/dist/routes/auth.js
|
|
657
659
|
import { Router } from "express";
|
|
@@ -4875,6 +4877,10 @@ function maskSettings(namespace, settings) {
|
|
|
4875
4877
|
}
|
|
4876
4878
|
async function getNamespaceSettings(req, res) {
|
|
4877
4879
|
const { namespace } = req.params;
|
|
4880
|
+
if (namespace.startsWith("addon:")) {
|
|
4881
|
+
res.status(403).json({ error: "Addon settings must be accessed through the add-ons API" });
|
|
4882
|
+
return;
|
|
4883
|
+
}
|
|
4878
4884
|
const settings = await getSettings(namespace);
|
|
4879
4885
|
res.json(maskSettings(namespace, settings));
|
|
4880
4886
|
}
|
|
@@ -4903,6 +4909,10 @@ async function getEditorialMode(_req, res) {
|
|
|
4903
4909
|
}
|
|
4904
4910
|
async function updateNamespaceSettings(req, res) {
|
|
4905
4911
|
const { namespace } = req.params;
|
|
4912
|
+
if (namespace.startsWith("addon:")) {
|
|
4913
|
+
res.status(403).json({ error: "Addon settings must be accessed through the add-ons API" });
|
|
4914
|
+
return;
|
|
4915
|
+
}
|
|
4906
4916
|
const incoming = req.body;
|
|
4907
4917
|
if (typeof incoming !== "object" || Array.isArray(incoming)) {
|
|
4908
4918
|
res.status(400).json({ error: "Body must be a flat key-value object" });
|
|
@@ -4935,7 +4945,10 @@ var PACKAGE_NAME = "@plank-cms/plank";
|
|
|
4935
4945
|
var CHANGELOG_BASE_URL = "https://github.com/plank-cms/plank/releases";
|
|
4936
4946
|
var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
|
|
4937
4947
|
var CACHE_TTL_MS = 1e3 * 60 * 30;
|
|
4938
|
-
var
|
|
4948
|
+
var packageJsonUrls = [
|
|
4949
|
+
new URL("../../package.json", import.meta.url),
|
|
4950
|
+
new URL("../package.json", import.meta.url)
|
|
4951
|
+
];
|
|
4939
4952
|
var cachedVersionCheck = null;
|
|
4940
4953
|
function normalizeVersion(value) {
|
|
4941
4954
|
return value.trim().replace(/^v/i, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -4992,17 +5005,26 @@ async function detectPackageManagerFromLockfiles() {
|
|
|
4992
5005
|
}
|
|
4993
5006
|
return null;
|
|
4994
5007
|
}
|
|
4995
|
-
async function
|
|
4996
|
-
const
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5008
|
+
async function getCurrentVersion() {
|
|
5009
|
+
for (const packageJsonUrl of packageJsonUrls) {
|
|
5010
|
+
try {
|
|
5011
|
+
const packageJsonPath = fileURLToPath2(packageJsonUrl);
|
|
5012
|
+
const raw = await readFile2(packageJsonPath, "utf8");
|
|
5013
|
+
const parsed = JSON.parse(raw);
|
|
5014
|
+
if (parsed.version) {
|
|
5015
|
+
return parsed.version;
|
|
5016
|
+
}
|
|
5017
|
+
} catch {
|
|
5018
|
+
continue;
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
return "0.0.0";
|
|
5000
5022
|
}
|
|
5001
5023
|
async function getVersionCheck() {
|
|
5002
5024
|
if (cachedVersionCheck && cachedVersionCheck.expiresAt > Date.now()) {
|
|
5003
5025
|
return cachedVersionCheck.value;
|
|
5004
5026
|
}
|
|
5005
|
-
const currentVersion = await
|
|
5027
|
+
const currentVersion = await getCurrentVersion();
|
|
5006
5028
|
const packageManager = await detectProjectPackageManager();
|
|
5007
5029
|
let latestVersion = null;
|
|
5008
5030
|
try {
|
|
@@ -5040,6 +5062,657 @@ async function getVersionInfo(_req, res) {
|
|
|
5040
5062
|
res.json(versionInfo);
|
|
5041
5063
|
}
|
|
5042
5064
|
|
|
5065
|
+
// ../core/dist/controllers/addons.js
|
|
5066
|
+
import { z as z9 } from "zod";
|
|
5067
|
+
|
|
5068
|
+
// ../core/dist/lib/addons.js
|
|
5069
|
+
import { access, readFile as readFile3 } from "fs/promises";
|
|
5070
|
+
import { createRequire } from "module";
|
|
5071
|
+
import { dirname as dirname2, join as join5, resolve } from "path";
|
|
5072
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5073
|
+
import { z as z8 } from "zod";
|
|
5074
|
+
var ADDON_PACKAGE_PREFIX = "@plank-cms/addon-";
|
|
5075
|
+
function getHostRequire() {
|
|
5076
|
+
return createRequire(join5(process.cwd(), "package.json"));
|
|
5077
|
+
}
|
|
5078
|
+
var addonSlotSchema = z8.object({
|
|
5079
|
+
id: z8.string().min(1),
|
|
5080
|
+
title: z8.string().min(1),
|
|
5081
|
+
order: z8.number().int().optional()
|
|
5082
|
+
});
|
|
5083
|
+
var addonManifestSchema = z8.object({
|
|
5084
|
+
id: z8.string().min(1),
|
|
5085
|
+
packageName: z8.string().min(1),
|
|
5086
|
+
name: z8.string().min(1),
|
|
5087
|
+
version: z8.string().min(1),
|
|
5088
|
+
plankRange: z8.string().min(1),
|
|
5089
|
+
description: z8.string().optional(),
|
|
5090
|
+
settingsNamespace: z8.string().min(1).optional(),
|
|
5091
|
+
slots: z8.object({
|
|
5092
|
+
dashboardWidgets: z8.array(addonSlotSchema).optional(),
|
|
5093
|
+
addonsSections: z8.array(addonSlotSchema).optional()
|
|
5094
|
+
}),
|
|
5095
|
+
admin: z8.object({
|
|
5096
|
+
entry: z8.string().min(1)
|
|
5097
|
+
}).optional()
|
|
5098
|
+
});
|
|
5099
|
+
var addonAdminFieldSchema = z8.discriminatedUnion("type", [
|
|
5100
|
+
z8.object({
|
|
5101
|
+
key: z8.string().min(1),
|
|
5102
|
+
type: z8.literal("contentTypesMultiSelect"),
|
|
5103
|
+
label: z8.string().min(1),
|
|
5104
|
+
description: z8.string().min(1),
|
|
5105
|
+
defaultValue: z8.array(z8.string())
|
|
5106
|
+
}),
|
|
5107
|
+
z8.object({
|
|
5108
|
+
key: z8.string().min(1),
|
|
5109
|
+
type: z8.literal("number"),
|
|
5110
|
+
label: z8.string().min(1),
|
|
5111
|
+
description: z8.string().min(1),
|
|
5112
|
+
min: z8.number(),
|
|
5113
|
+
defaultValue: z8.number()
|
|
5114
|
+
})
|
|
5115
|
+
]);
|
|
5116
|
+
var addonAdminModuleSchema = z8.object({
|
|
5117
|
+
addonId: z8.string().min(1),
|
|
5118
|
+
title: z8.string().min(1),
|
|
5119
|
+
description: z8.string().min(1),
|
|
5120
|
+
settingsNamespace: z8.string().min(1),
|
|
5121
|
+
checks: z8.array(z8.object({
|
|
5122
|
+
id: z8.string().min(1),
|
|
5123
|
+
label: z8.string().min(1),
|
|
5124
|
+
description: z8.string().min(1)
|
|
5125
|
+
})),
|
|
5126
|
+
settings: z8.object({
|
|
5127
|
+
title: z8.string().min(1),
|
|
5128
|
+
description: z8.string().min(1),
|
|
5129
|
+
fields: z8.array(addonAdminFieldSchema)
|
|
5130
|
+
})
|
|
5131
|
+
});
|
|
5132
|
+
function normalizeSlot(slot) {
|
|
5133
|
+
return {
|
|
5134
|
+
id: slot.id,
|
|
5135
|
+
title: slot.title,
|
|
5136
|
+
order: slot.order ?? 100
|
|
5137
|
+
};
|
|
5138
|
+
}
|
|
5139
|
+
function compareSlotOrder(left, right) {
|
|
5140
|
+
if ((left.order ?? 100) !== (right.order ?? 100)) {
|
|
5141
|
+
return (left.order ?? 100) - (right.order ?? 100);
|
|
5142
|
+
}
|
|
5143
|
+
return left.id.localeCompare(right.id);
|
|
5144
|
+
}
|
|
5145
|
+
function normalizeAddonSlots(manifest) {
|
|
5146
|
+
const dashboardWidgets = (manifest.slots.dashboardWidgets ?? []).map(normalizeSlot).sort(compareSlotOrder);
|
|
5147
|
+
const addonsSections = manifest.admin?.entry ? (manifest.slots.addonsSections ?? []).map(normalizeSlot).sort(compareSlotOrder) : [];
|
|
5148
|
+
return {
|
|
5149
|
+
dashboardWidgets,
|
|
5150
|
+
addonsSections
|
|
5151
|
+
};
|
|
5152
|
+
}
|
|
5153
|
+
function createFallbackAddonId(packageName) {
|
|
5154
|
+
return packageName.startsWith(ADDON_PACKAGE_PREFIX) ? packageName.slice(ADDON_PACKAGE_PREFIX.length) : packageName;
|
|
5155
|
+
}
|
|
5156
|
+
async function pathExists(path) {
|
|
5157
|
+
try {
|
|
5158
|
+
await access(path);
|
|
5159
|
+
return true;
|
|
5160
|
+
} catch {
|
|
5161
|
+
return false;
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
async function resolvePackageJsonPath(packageName) {
|
|
5165
|
+
const hostRequire = getHostRequire();
|
|
5166
|
+
try {
|
|
5167
|
+
return hostRequire.resolve(`${packageName}/package.json`);
|
|
5168
|
+
} catch {
|
|
5169
|
+
try {
|
|
5170
|
+
const entryPath = hostRequire.resolve(packageName);
|
|
5171
|
+
let currentDir = dirname2(entryPath);
|
|
5172
|
+
for (; ; ) {
|
|
5173
|
+
const candidate = join5(currentDir, "package.json");
|
|
5174
|
+
if (await pathExists(candidate)) {
|
|
5175
|
+
return candidate;
|
|
5176
|
+
}
|
|
5177
|
+
const parentDir = dirname2(currentDir);
|
|
5178
|
+
if (parentDir === currentDir)
|
|
5179
|
+
return null;
|
|
5180
|
+
currentDir = parentDir;
|
|
5181
|
+
}
|
|
5182
|
+
} catch {
|
|
5183
|
+
return null;
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
async function resolvePackageRoot(packageName) {
|
|
5188
|
+
const packageJsonPath = await resolvePackageJsonPath(packageName);
|
|
5189
|
+
if (packageJsonPath)
|
|
5190
|
+
return dirname2(packageJsonPath);
|
|
5191
|
+
try {
|
|
5192
|
+
const manifestUrl = import.meta.resolve(`${packageName}/plank`);
|
|
5193
|
+
let currentDir = dirname2(fileURLToPath3(manifestUrl));
|
|
5194
|
+
for (; ; ) {
|
|
5195
|
+
const candidate = join5(currentDir, "package.json");
|
|
5196
|
+
if (await pathExists(candidate)) {
|
|
5197
|
+
return currentDir;
|
|
5198
|
+
}
|
|
5199
|
+
const parentDir = dirname2(currentDir);
|
|
5200
|
+
if (parentDir === currentDir)
|
|
5201
|
+
return null;
|
|
5202
|
+
currentDir = parentDir;
|
|
5203
|
+
}
|
|
5204
|
+
} catch {
|
|
5205
|
+
return null;
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
async function readInstalledPackageJson(packageName) {
|
|
5209
|
+
const packageJsonPath = await resolvePackageJsonPath(packageName);
|
|
5210
|
+
if (!packageJsonPath)
|
|
5211
|
+
return null;
|
|
5212
|
+
try {
|
|
5213
|
+
const raw = await readFile3(packageJsonPath, "utf8");
|
|
5214
|
+
return JSON.parse(raw);
|
|
5215
|
+
} catch {
|
|
5216
|
+
return null;
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
async function readHostPackageJson() {
|
|
5220
|
+
const raw = await readFile3(join5(process.cwd(), "package.json"), "utf8");
|
|
5221
|
+
return JSON.parse(raw);
|
|
5222
|
+
}
|
|
5223
|
+
function listDeclaredAddonPackages(packageJson) {
|
|
5224
|
+
return Array.from(/* @__PURE__ */ new Set([
|
|
5225
|
+
...Object.keys(packageJson.dependencies ?? {}),
|
|
5226
|
+
...Object.keys(packageJson.optionalDependencies ?? {})
|
|
5227
|
+
])).filter((packageName) => packageName.startsWith(ADDON_PACKAGE_PREFIX)).sort();
|
|
5228
|
+
}
|
|
5229
|
+
function normalizeVersion2(value) {
|
|
5230
|
+
const [major = 0, minor = 0, patch = 0] = value.trim().replace(/^v/i, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
5231
|
+
return [major, minor, patch];
|
|
5232
|
+
}
|
|
5233
|
+
function compareVersions2(left, right) {
|
|
5234
|
+
const leftParts = normalizeVersion2(left);
|
|
5235
|
+
const rightParts = normalizeVersion2(right);
|
|
5236
|
+
for (let index = 0; index < 3; index += 1) {
|
|
5237
|
+
if (leftParts[index] > rightParts[index])
|
|
5238
|
+
return 1;
|
|
5239
|
+
if (leftParts[index] < rightParts[index])
|
|
5240
|
+
return -1;
|
|
5241
|
+
}
|
|
5242
|
+
return 0;
|
|
5243
|
+
}
|
|
5244
|
+
function buildCaretUpperBound(version) {
|
|
5245
|
+
const [major, minor, patch] = normalizeVersion2(version);
|
|
5246
|
+
if (major > 0)
|
|
5247
|
+
return `${major + 1}.0.0`;
|
|
5248
|
+
if (minor > 0)
|
|
5249
|
+
return `0.${minor + 1}.0`;
|
|
5250
|
+
return `0.0.${patch + 1}`;
|
|
5251
|
+
}
|
|
5252
|
+
function buildTildeUpperBound(version) {
|
|
5253
|
+
const [major, minor] = normalizeVersion2(version);
|
|
5254
|
+
return `${major}.${minor + 1}.0`;
|
|
5255
|
+
}
|
|
5256
|
+
function satisfiesComparator(version, comparator) {
|
|
5257
|
+
const value = comparator.trim();
|
|
5258
|
+
if (!value)
|
|
5259
|
+
return true;
|
|
5260
|
+
if (value.startsWith(">="))
|
|
5261
|
+
return compareVersions2(version, value.slice(2)) >= 0;
|
|
5262
|
+
if (value.startsWith("<="))
|
|
5263
|
+
return compareVersions2(version, value.slice(2)) <= 0;
|
|
5264
|
+
if (value.startsWith(">"))
|
|
5265
|
+
return compareVersions2(version, value.slice(1)) > 0;
|
|
5266
|
+
if (value.startsWith("<"))
|
|
5267
|
+
return compareVersions2(version, value.slice(1)) < 0;
|
|
5268
|
+
if (value.startsWith("^")) {
|
|
5269
|
+
const lower = value.slice(1);
|
|
5270
|
+
return compareVersions2(version, lower) >= 0 && compareVersions2(version, buildCaretUpperBound(lower)) < 0;
|
|
5271
|
+
}
|
|
5272
|
+
if (value.startsWith("~")) {
|
|
5273
|
+
const lower = value.slice(1);
|
|
5274
|
+
return compareVersions2(version, lower) >= 0 && compareVersions2(version, buildTildeUpperBound(lower)) < 0;
|
|
5275
|
+
}
|
|
5276
|
+
return compareVersions2(version, value) === 0;
|
|
5277
|
+
}
|
|
5278
|
+
function satisfiesVersionRange(version, range) {
|
|
5279
|
+
const normalizedRange = range.trim();
|
|
5280
|
+
if (!normalizedRange || normalizedRange === "*")
|
|
5281
|
+
return true;
|
|
5282
|
+
return normalizedRange.split("||").some((part) => part.trim().split(/\s+/).every((token) => satisfiesComparator(version, token)));
|
|
5283
|
+
}
|
|
5284
|
+
async function loadAddonManifest(packageName) {
|
|
5285
|
+
const module = await import(`${packageName}/plank`);
|
|
5286
|
+
const parsed = addonManifestSchema.safeParse(module?.manifest);
|
|
5287
|
+
if (!parsed.success) {
|
|
5288
|
+
throw new Error(parsed.error.issues.map((issue) => issue.message).join(", "));
|
|
5289
|
+
}
|
|
5290
|
+
if (parsed.data.packageName !== packageName) {
|
|
5291
|
+
throw new Error(`Manifest packageName mismatch for ${packageName}`);
|
|
5292
|
+
}
|
|
5293
|
+
return parsed.data;
|
|
5294
|
+
}
|
|
5295
|
+
async function loadAddonAdminModule(packageName) {
|
|
5296
|
+
const module = await import(`${packageName}/admin`);
|
|
5297
|
+
const candidate = module?.adminModule ?? module?.default;
|
|
5298
|
+
const parsed = addonAdminModuleSchema.safeParse(candidate);
|
|
5299
|
+
if (!parsed.success) {
|
|
5300
|
+
throw new Error(parsed.error.issues.map((issue) => issue.message).join(", "));
|
|
5301
|
+
}
|
|
5302
|
+
return parsed.data;
|
|
5303
|
+
}
|
|
5304
|
+
async function loadAddonServerModule(packageName) {
|
|
5305
|
+
const module = await import(`${packageName}/server`);
|
|
5306
|
+
const candidate = module?.serverModule ?? module?.default;
|
|
5307
|
+
if (!candidate || typeof candidate.runAction !== "function") {
|
|
5308
|
+
throw new Error(`Invalid server module for ${packageName}`);
|
|
5309
|
+
}
|
|
5310
|
+
return candidate;
|
|
5311
|
+
}
|
|
5312
|
+
async function resolveAddonAdminEntryPath(packageName) {
|
|
5313
|
+
const [manifest, packageRoot] = await Promise.all([
|
|
5314
|
+
loadAddonManifest(packageName),
|
|
5315
|
+
resolvePackageRoot(packageName)
|
|
5316
|
+
]);
|
|
5317
|
+
if (!manifest.admin?.entry || !packageRoot)
|
|
5318
|
+
return null;
|
|
5319
|
+
const entryPath = resolve(packageRoot, manifest.admin.entry);
|
|
5320
|
+
if (!entryPath.startsWith(packageRoot)) {
|
|
5321
|
+
throw new Error(`Invalid admin entry path for ${packageName}`);
|
|
5322
|
+
}
|
|
5323
|
+
if (!await pathExists(entryPath)) {
|
|
5324
|
+
throw new Error(`Admin entry file not found for ${packageName}: ${entryPath}`);
|
|
5325
|
+
}
|
|
5326
|
+
return entryPath;
|
|
5327
|
+
}
|
|
5328
|
+
async function discoverAddon(packageName, coreVersion) {
|
|
5329
|
+
const packageJson = await readInstalledPackageJson(packageName);
|
|
5330
|
+
try {
|
|
5331
|
+
const manifest = await loadAddonManifest(packageName);
|
|
5332
|
+
const compatible = satisfiesVersionRange(coreVersion, manifest.plankRange);
|
|
5333
|
+
return {
|
|
5334
|
+
id: manifest.id,
|
|
5335
|
+
packageName,
|
|
5336
|
+
name: manifest.name,
|
|
5337
|
+
version: manifest.version,
|
|
5338
|
+
plankRange: manifest.plankRange,
|
|
5339
|
+
description: manifest.description ?? packageJson?.description ?? null,
|
|
5340
|
+
installed: true,
|
|
5341
|
+
enabled: false,
|
|
5342
|
+
compatible,
|
|
5343
|
+
hasAdminUi: Boolean(manifest.admin?.entry),
|
|
5344
|
+
settingsNamespace: manifest.settingsNamespace ?? null,
|
|
5345
|
+
slots: normalizeAddonSlots(manifest)
|
|
5346
|
+
};
|
|
5347
|
+
} catch (error) {
|
|
5348
|
+
const fallbackId = createFallbackAddonId(packageName);
|
|
5349
|
+
const message = error instanceof Error ? error.message : "Unknown manifest error";
|
|
5350
|
+
console.warn(`[plank/addons] Skipping invalid add-on manifest for ${packageName}: ${message}`);
|
|
5351
|
+
return {
|
|
5352
|
+
id: fallbackId,
|
|
5353
|
+
packageName,
|
|
5354
|
+
name: packageJson?.name ?? fallbackId,
|
|
5355
|
+
version: packageJson?.version ?? null,
|
|
5356
|
+
plankRange: null,
|
|
5357
|
+
description: packageJson?.description ?? null,
|
|
5358
|
+
installed: true,
|
|
5359
|
+
enabled: false,
|
|
5360
|
+
compatible: false,
|
|
5361
|
+
hasAdminUi: false,
|
|
5362
|
+
settingsNamespace: null,
|
|
5363
|
+
slots: {
|
|
5364
|
+
dashboardWidgets: [],
|
|
5365
|
+
addonsSections: []
|
|
5366
|
+
}
|
|
5367
|
+
};
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
async function syncInstalledAddons() {
|
|
5371
|
+
const hostPackageJson = await readHostPackageJson();
|
|
5372
|
+
const packageNames = listDeclaredAddonPackages(hostPackageJson);
|
|
5373
|
+
const coreVersion = await getCurrentVersion();
|
|
5374
|
+
const discovered = (await Promise.all(packageNames.map((packageName) => discoverAddon(packageName, coreVersion)))).filter((addon) => addon !== null);
|
|
5375
|
+
const client = await pool_default.connect();
|
|
5376
|
+
try {
|
|
5377
|
+
await client.query("BEGIN");
|
|
5378
|
+
for (const addon of discovered) {
|
|
5379
|
+
await client.query(`INSERT INTO plank_addons (
|
|
5380
|
+
id,
|
|
5381
|
+
package_name,
|
|
5382
|
+
name,
|
|
5383
|
+
version,
|
|
5384
|
+
plank_range,
|
|
5385
|
+
description,
|
|
5386
|
+
installed,
|
|
5387
|
+
compatible,
|
|
5388
|
+
has_admin_ui,
|
|
5389
|
+
settings_namespace,
|
|
5390
|
+
slots_json
|
|
5391
|
+
)
|
|
5392
|
+
VALUES ($1, $2, $3, $4, $5, $6, TRUE, $7, $8, $9, $10::jsonb)
|
|
5393
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
5394
|
+
package_name = EXCLUDED.package_name,
|
|
5395
|
+
name = EXCLUDED.name,
|
|
5396
|
+
version = EXCLUDED.version,
|
|
5397
|
+
plank_range = EXCLUDED.plank_range,
|
|
5398
|
+
description = EXCLUDED.description,
|
|
5399
|
+
installed = EXCLUDED.installed,
|
|
5400
|
+
compatible = EXCLUDED.compatible,
|
|
5401
|
+
has_admin_ui = EXCLUDED.has_admin_ui,
|
|
5402
|
+
settings_namespace = EXCLUDED.settings_namespace,
|
|
5403
|
+
slots_json = EXCLUDED.slots_json,
|
|
5404
|
+
updated_at = NOW()`, [
|
|
5405
|
+
addon.id,
|
|
5406
|
+
addon.packageName,
|
|
5407
|
+
addon.name,
|
|
5408
|
+
addon.version,
|
|
5409
|
+
addon.plankRange,
|
|
5410
|
+
addon.description,
|
|
5411
|
+
addon.compatible,
|
|
5412
|
+
addon.hasAdminUi,
|
|
5413
|
+
addon.settingsNamespace,
|
|
5414
|
+
JSON.stringify(addon.slots)
|
|
5415
|
+
]);
|
|
5416
|
+
}
|
|
5417
|
+
if (packageNames.length > 0) {
|
|
5418
|
+
await client.query(`UPDATE plank_addons
|
|
5419
|
+
SET installed = FALSE,
|
|
5420
|
+
compatible = FALSE,
|
|
5421
|
+
has_admin_ui = FALSE,
|
|
5422
|
+
slots_json = '{}'::jsonb,
|
|
5423
|
+
updated_at = NOW()
|
|
5424
|
+
WHERE package_name LIKE $1
|
|
5425
|
+
AND package_name <> ALL($2::text[])`, [`${ADDON_PACKAGE_PREFIX}%`, packageNames]);
|
|
5426
|
+
} else {
|
|
5427
|
+
await client.query(`UPDATE plank_addons
|
|
5428
|
+
SET installed = FALSE,
|
|
5429
|
+
compatible = FALSE,
|
|
5430
|
+
has_admin_ui = FALSE,
|
|
5431
|
+
slots_json = '{}'::jsonb,
|
|
5432
|
+
updated_at = NOW()
|
|
5433
|
+
WHERE package_name LIKE $1`, [`${ADDON_PACKAGE_PREFIX}%`]);
|
|
5434
|
+
}
|
|
5435
|
+
await client.query("COMMIT");
|
|
5436
|
+
} catch (error) {
|
|
5437
|
+
await client.query("ROLLBACK");
|
|
5438
|
+
throw error;
|
|
5439
|
+
} finally {
|
|
5440
|
+
client.release();
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
function normalizeSlotsFromRow(value) {
|
|
5444
|
+
return {
|
|
5445
|
+
dashboardWidgets: Array.isArray(value?.dashboardWidgets) ? value.dashboardWidgets : [],
|
|
5446
|
+
addonsSections: Array.isArray(value?.addonsSections) ? value.addonsSections : []
|
|
5447
|
+
};
|
|
5448
|
+
}
|
|
5449
|
+
function mapAddonRow(row) {
|
|
5450
|
+
return {
|
|
5451
|
+
id: row.id,
|
|
5452
|
+
packageName: row.package_name,
|
|
5453
|
+
name: row.name,
|
|
5454
|
+
version: row.version ?? "",
|
|
5455
|
+
...row.description ? { description: row.description } : {},
|
|
5456
|
+
installed: row.installed,
|
|
5457
|
+
enabled: row.enabled,
|
|
5458
|
+
compatible: row.compatible,
|
|
5459
|
+
hasAdminUi: row.has_admin_ui,
|
|
5460
|
+
...row.settings_namespace ? { settingsNamespace: row.settings_namespace } : {},
|
|
5461
|
+
...row.has_admin_ui ? { adminUrl: `/admin/add-ons/${row.id}` } : {}
|
|
5462
|
+
};
|
|
5463
|
+
}
|
|
5464
|
+
async function listAddonRows() {
|
|
5465
|
+
const { rows } = await pool_default.query(`SELECT
|
|
5466
|
+
id,
|
|
5467
|
+
package_name,
|
|
5468
|
+
name,
|
|
5469
|
+
version,
|
|
5470
|
+
plank_range,
|
|
5471
|
+
description,
|
|
5472
|
+
installed,
|
|
5473
|
+
enabled,
|
|
5474
|
+
compatible,
|
|
5475
|
+
has_admin_ui,
|
|
5476
|
+
settings_namespace,
|
|
5477
|
+
slots_json
|
|
5478
|
+
FROM plank_addons
|
|
5479
|
+
ORDER BY name ASC, id ASC`);
|
|
5480
|
+
return rows;
|
|
5481
|
+
}
|
|
5482
|
+
async function getAddonRow(id) {
|
|
5483
|
+
const { rows } = await pool_default.query(`SELECT
|
|
5484
|
+
id,
|
|
5485
|
+
package_name,
|
|
5486
|
+
name,
|
|
5487
|
+
version,
|
|
5488
|
+
plank_range,
|
|
5489
|
+
description,
|
|
5490
|
+
installed,
|
|
5491
|
+
enabled,
|
|
5492
|
+
compatible,
|
|
5493
|
+
has_admin_ui,
|
|
5494
|
+
settings_namespace,
|
|
5495
|
+
slots_json
|
|
5496
|
+
FROM plank_addons
|
|
5497
|
+
WHERE id = $1`, [id]);
|
|
5498
|
+
return rows[0] ?? null;
|
|
5499
|
+
}
|
|
5500
|
+
async function getAddonAdminModule(id) {
|
|
5501
|
+
const addon = await getAddonRow(id);
|
|
5502
|
+
if (!addon || !addon.installed || !addon.has_admin_ui)
|
|
5503
|
+
return null;
|
|
5504
|
+
try {
|
|
5505
|
+
return await loadAddonAdminModule(addon.package_name);
|
|
5506
|
+
} catch (error) {
|
|
5507
|
+
const message = error instanceof Error ? error.message : "Unknown admin module error";
|
|
5508
|
+
console.warn(`[plank/addons] Failed to load admin module for ${addon.package_name}: ${message}`);
|
|
5509
|
+
return null;
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
async function getAddonAdminEntryPath(id) {
|
|
5513
|
+
const addon = await getAddonRow(id);
|
|
5514
|
+
if (!addon || !addon.installed || !addon.has_admin_ui)
|
|
5515
|
+
return null;
|
|
5516
|
+
try {
|
|
5517
|
+
return await resolveAddonAdminEntryPath(addon.package_name);
|
|
5518
|
+
} catch (error) {
|
|
5519
|
+
const message = error instanceof Error ? error.message : "Unknown admin entry error";
|
|
5520
|
+
console.warn(`[plank/addons] Failed to resolve admin entry for ${addon.package_name}: ${message}`);
|
|
5521
|
+
return null;
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
async function runAddonServerAction(id, action, input) {
|
|
5525
|
+
const addon = await getAddonRow(id);
|
|
5526
|
+
if (!addon) {
|
|
5527
|
+
throw new Error("Addon not found");
|
|
5528
|
+
}
|
|
5529
|
+
if (!addon.installed) {
|
|
5530
|
+
throw new Error("Addon is not installed");
|
|
5531
|
+
}
|
|
5532
|
+
if (!addon.enabled) {
|
|
5533
|
+
throw new Error("Addon is disabled");
|
|
5534
|
+
}
|
|
5535
|
+
if (!addon.compatible) {
|
|
5536
|
+
throw new Error("Addon is not compatible with this Plank version");
|
|
5537
|
+
}
|
|
5538
|
+
const serverModule = await loadAddonServerModule(addon.package_name);
|
|
5539
|
+
return serverModule.runAction({
|
|
5540
|
+
action,
|
|
5541
|
+
input,
|
|
5542
|
+
addon: {
|
|
5543
|
+
id: addon.id,
|
|
5544
|
+
packageName: addon.package_name,
|
|
5545
|
+
settingsNamespace: addon.settings_namespace
|
|
5546
|
+
},
|
|
5547
|
+
context: {
|
|
5548
|
+
db: {
|
|
5549
|
+
query: pool_default.query.bind(pool_default)
|
|
5550
|
+
},
|
|
5551
|
+
getSettings,
|
|
5552
|
+
findAllContentTypes,
|
|
5553
|
+
findContentTypeBySlug,
|
|
5554
|
+
quoteIdentifier
|
|
5555
|
+
}
|
|
5556
|
+
});
|
|
5557
|
+
}
|
|
5558
|
+
async function updateAddonEnabled(id, enabled) {
|
|
5559
|
+
const { rows } = await pool_default.query(`UPDATE plank_addons
|
|
5560
|
+
SET enabled = $2, updated_at = NOW()
|
|
5561
|
+
WHERE id = $1
|
|
5562
|
+
RETURNING
|
|
5563
|
+
id,
|
|
5564
|
+
package_name,
|
|
5565
|
+
name,
|
|
5566
|
+
version,
|
|
5567
|
+
plank_range,
|
|
5568
|
+
description,
|
|
5569
|
+
installed,
|
|
5570
|
+
enabled,
|
|
5571
|
+
compatible,
|
|
5572
|
+
has_admin_ui,
|
|
5573
|
+
settings_namespace,
|
|
5574
|
+
slots_json`, [id, enabled]);
|
|
5575
|
+
return rows[0] ?? null;
|
|
5576
|
+
}
|
|
5577
|
+
async function buildAdminAddonsRegistry() {
|
|
5578
|
+
const rows = await listAddonRows();
|
|
5579
|
+
const enabledRows = rows.filter((row) => row.installed && row.enabled && row.compatible);
|
|
5580
|
+
return {
|
|
5581
|
+
addons: rows.map(mapAddonRow),
|
|
5582
|
+
slots: {
|
|
5583
|
+
dashboardWidgets: enabledRows.flatMap((row) => normalizeSlotsFromRow(row.slots_json).dashboardWidgets.map((slot) => ({
|
|
5584
|
+
addonId: row.id,
|
|
5585
|
+
slotId: slot.id,
|
|
5586
|
+
title: slot.title,
|
|
5587
|
+
order: slot.order ?? 100
|
|
5588
|
+
}))),
|
|
5589
|
+
addonsSections: enabledRows.flatMap((row) => normalizeSlotsFromRow(row.slots_json).addonsSections.map((slot) => ({
|
|
5590
|
+
addonId: row.id,
|
|
5591
|
+
slotId: slot.id,
|
|
5592
|
+
title: slot.title,
|
|
5593
|
+
order: slot.order ?? 100
|
|
5594
|
+
})))
|
|
5595
|
+
}
|
|
5596
|
+
};
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
// ../core/dist/controllers/addons.js
|
|
5600
|
+
function mapAddon(row) {
|
|
5601
|
+
return {
|
|
5602
|
+
id: row.id,
|
|
5603
|
+
packageName: row.package_name,
|
|
5604
|
+
name: row.name,
|
|
5605
|
+
version: row.version ?? "",
|
|
5606
|
+
...row.description ? { description: row.description } : {},
|
|
5607
|
+
installed: row.installed,
|
|
5608
|
+
enabled: row.enabled,
|
|
5609
|
+
compatible: row.compatible,
|
|
5610
|
+
hasAdminUi: row.has_admin_ui,
|
|
5611
|
+
...row.settings_namespace ? { settingsNamespace: row.settings_namespace } : {}
|
|
5612
|
+
};
|
|
5613
|
+
}
|
|
5614
|
+
async function getSettingsAddon(id) {
|
|
5615
|
+
const addon = await getAddonRow(id);
|
|
5616
|
+
if (!addon)
|
|
5617
|
+
return { error: "Addon not found" };
|
|
5618
|
+
if (!addon.settings_namespace)
|
|
5619
|
+
return { error: "Addon does not expose settings" };
|
|
5620
|
+
return { addon };
|
|
5621
|
+
}
|
|
5622
|
+
async function getAddonsRegistry(_req, res) {
|
|
5623
|
+
const registry = await buildAdminAddonsRegistry();
|
|
5624
|
+
res.json(registry);
|
|
5625
|
+
}
|
|
5626
|
+
async function listAddons(_req, res) {
|
|
5627
|
+
const addons = await listAddonRows();
|
|
5628
|
+
res.json(addons.map(mapAddon));
|
|
5629
|
+
}
|
|
5630
|
+
async function enableAddon(req, res) {
|
|
5631
|
+
const addon = await getAddonRow(req.params.id);
|
|
5632
|
+
if (!addon) {
|
|
5633
|
+
res.status(404).json({ error: "Addon not found" });
|
|
5634
|
+
return;
|
|
5635
|
+
}
|
|
5636
|
+
if (!addon.installed) {
|
|
5637
|
+
res.status(409).json({ error: "Addon is not installed" });
|
|
5638
|
+
return;
|
|
5639
|
+
}
|
|
5640
|
+
if (!addon.compatible) {
|
|
5641
|
+
res.status(409).json({ error: "Addon is not compatible with this Plank version" });
|
|
5642
|
+
return;
|
|
5643
|
+
}
|
|
5644
|
+
const updated = await updateAddonEnabled(req.params.id, true);
|
|
5645
|
+
res.json(mapAddon(updated));
|
|
5646
|
+
}
|
|
5647
|
+
async function disableAddon(req, res) {
|
|
5648
|
+
const addon = await getAddonRow(req.params.id);
|
|
5649
|
+
if (!addon) {
|
|
5650
|
+
res.status(404).json({ error: "Addon not found" });
|
|
5651
|
+
return;
|
|
5652
|
+
}
|
|
5653
|
+
const updated = await updateAddonEnabled(req.params.id, false);
|
|
5654
|
+
res.json(mapAddon(updated));
|
|
5655
|
+
}
|
|
5656
|
+
async function getAddonSettings(req, res) {
|
|
5657
|
+
const result = await getSettingsAddon(req.params.id);
|
|
5658
|
+
if ("error" in result) {
|
|
5659
|
+
res.status(result.error === "Addon not found" ? 404 : 400).json({ error: result.error });
|
|
5660
|
+
return;
|
|
5661
|
+
}
|
|
5662
|
+
const settings = await getSettings(result.addon.settings_namespace);
|
|
5663
|
+
res.json(settings);
|
|
5664
|
+
}
|
|
5665
|
+
async function getAddonAdminModuleDefinition(req, res) {
|
|
5666
|
+
const addon = await getAddonAdminModule(req.params.id);
|
|
5667
|
+
if (!addon) {
|
|
5668
|
+
res.status(404).json({ error: "Addon admin module not found" });
|
|
5669
|
+
return;
|
|
5670
|
+
}
|
|
5671
|
+
res.json(addon);
|
|
5672
|
+
}
|
|
5673
|
+
async function getAddonAdminEntry(req, res) {
|
|
5674
|
+
const entryPath = await getAddonAdminEntryPath(req.params.id);
|
|
5675
|
+
if (!entryPath) {
|
|
5676
|
+
res.status(404).json({ error: "Addon admin entry not found" });
|
|
5677
|
+
return;
|
|
5678
|
+
}
|
|
5679
|
+
res.type("application/javascript");
|
|
5680
|
+
res.sendFile(entryPath);
|
|
5681
|
+
}
|
|
5682
|
+
async function runAddonAction(req, res) {
|
|
5683
|
+
const parsed = z9.object({
|
|
5684
|
+
action: z9.string().min(1),
|
|
5685
|
+
input: z9.unknown().optional()
|
|
5686
|
+
}).safeParse(req.body);
|
|
5687
|
+
if (!parsed.success) {
|
|
5688
|
+
res.status(400).json({ error: "Body must include a valid action name" });
|
|
5689
|
+
return;
|
|
5690
|
+
}
|
|
5691
|
+
try {
|
|
5692
|
+
const result = await runAddonServerAction(req.params.id, parsed.data.action, parsed.data.input);
|
|
5693
|
+
res.json({ result });
|
|
5694
|
+
} catch (error) {
|
|
5695
|
+
const message = error instanceof Error ? error.message : "Could not run add-on action";
|
|
5696
|
+
const status = message === "Addon not found" ? 404 : message === "Addon is not installed" ? 409 : message === "Addon is disabled" || message === "Addon is not compatible with this Plank version" ? 409 : 400;
|
|
5697
|
+
res.status(status).json({ error: message });
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
async function updateAddonSettings(req, res) {
|
|
5701
|
+
const result = await getSettingsAddon(req.params.id);
|
|
5702
|
+
if ("error" in result) {
|
|
5703
|
+
res.status(result.error === "Addon not found" ? 404 : 400).json({ error: result.error });
|
|
5704
|
+
return;
|
|
5705
|
+
}
|
|
5706
|
+
const parsed = z9.record(z9.string(), z9.string()).safeParse(req.body);
|
|
5707
|
+
if (!parsed.success) {
|
|
5708
|
+
res.status(400).json({ error: "Body must be a flat key-value object" });
|
|
5709
|
+
return;
|
|
5710
|
+
}
|
|
5711
|
+
await setSettings(result.addon.settings_namespace, parsed.data);
|
|
5712
|
+
const settings = await getSettings(result.addon.settings_namespace);
|
|
5713
|
+
res.json(settings);
|
|
5714
|
+
}
|
|
5715
|
+
|
|
5043
5716
|
// ../core/dist/routes/admin.js
|
|
5044
5717
|
var router2 = Router2();
|
|
5045
5718
|
router2.use(authenticate);
|
|
@@ -5098,6 +5771,15 @@ router2.get("/editorial-mode", getEditorialMode);
|
|
|
5098
5771
|
router2.get("/version", getVersionInfo);
|
|
5099
5772
|
router2.get("/settings/:namespace", authorize("settings:overview:read"), getNamespaceSettings);
|
|
5100
5773
|
router2.put("/settings/:namespace", authorize("settings:overview:write"), updateNamespaceSettings);
|
|
5774
|
+
router2.get("/addons/registry", authorize("addons:read"), getAddonsRegistry);
|
|
5775
|
+
router2.get("/addons", authorize("addons:read"), listAddons);
|
|
5776
|
+
router2.post("/addons/:id/enable", authorize("addons:write"), enableAddon);
|
|
5777
|
+
router2.post("/addons/:id/disable", authorize("addons:write"), disableAddon);
|
|
5778
|
+
router2.get("/addons/:id/admin-module", authorize("addons:read"), getAddonAdminModuleDefinition);
|
|
5779
|
+
router2.get("/addons/:id/admin-entry.js", authorize("addons:read"), getAddonAdminEntry);
|
|
5780
|
+
router2.post("/addons/:id/actions", authorize("addons:read"), runAddonAction);
|
|
5781
|
+
router2.get("/addons/:id/settings", authorize("addons:read"), getAddonSettings);
|
|
5782
|
+
router2.put("/addons/:id/settings", authorize("addons:write"), updateAddonSettings);
|
|
5101
5783
|
router2.get("/webhooks", authorize("settings:webhooks:read"), listWebhooks);
|
|
5102
5784
|
router2.post("/webhooks", authorize("settings:webhooks:write"), createWebhook);
|
|
5103
5785
|
router2.delete("/webhooks/:id", authorize("settings:webhooks:delete"), deleteWebhook);
|
|
@@ -5809,9 +6491,9 @@ if (isDev) {
|
|
|
5809
6491
|
app.get("/admin/*path", (_req, res) => res.redirect(adminDevUrl));
|
|
5810
6492
|
app.get("/admin", (_req, res) => res.redirect(adminDevUrl));
|
|
5811
6493
|
} else {
|
|
5812
|
-
const adminDist = process.env.PLANK_ADMIN_DIST ??
|
|
6494
|
+
const adminDist = process.env.PLANK_ADMIN_DIST ?? join6(dirname3(fileURLToPath4(import.meta.url)), "../public/admin");
|
|
5813
6495
|
app.use("/admin", express.static(adminDist));
|
|
5814
|
-
app.get("/admin/*path", (_req, res) => res.sendFile(
|
|
6496
|
+
app.get("/admin/*path", (_req, res) => res.sendFile(join6(adminDist, "index.html")));
|
|
5815
6497
|
}
|
|
5816
6498
|
app.use(errorHandler);
|
|
5817
6499
|
var app_default = app;
|
|
@@ -5822,6 +6504,7 @@ async function start() {
|
|
|
5822
6504
|
const isDev2 = !process.env.PLANK_ADMIN_DIST;
|
|
5823
6505
|
await migrate();
|
|
5824
6506
|
await syncAllTables();
|
|
6507
|
+
await syncInstalledAddons();
|
|
5825
6508
|
app_default.listen(PORT2, () => {
|
|
5826
6509
|
const coreBase = `http://localhost:${PORT2}`;
|
|
5827
6510
|
const adminUrl = isDev2 ? "http://localhost:3000" : `${coreBase}/admin`;
|