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