domma-cms 0.55.1 → 0.69.3
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/CLAUDE.md +194 -3
- package/README.md +18 -14
- package/admin/css/admin.css +1 -1
- package/admin/css/contacts.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-code.html +1 -1
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +16 -97
- package/admin/js/templates/theme.html +211 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +28 -24
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +6 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +27 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/server.json +3 -1
- package/package.json +14 -10
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4473 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1022 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -0
- package/plugins/blog/admin/templates/blog.html +35 -41
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +63 -40
- package/plugins/blog/admin/templates/settings.html +3 -9
- package/plugins/blog/admin/views/blog.js +288 -159
- package/plugins/blog/admin/views/categories.js +212 -206
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +306 -39
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -84
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +16 -13
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +259 -26
- package/plugins/blog/plugin.json +21 -73
- package/plugins/blog/plugin.public.js +217 -0
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +34 -0
- package/public/css/forms.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +77 -4
- package/scripts/copy-domma.js +48 -0
- package/scripts/run-tests.mjs +92 -0
- package/scripts/seed.js +1996 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/contacts.js +506 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/dashboard.js +8 -2
- package/server/routes/api/forms.js +42 -3
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +198 -9
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/api/todo.js +178 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +135 -1
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/collections.js +13 -3
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +124 -10
- package/server/services/permissionRegistry.js +74 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +340 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +224 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +580 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/analytics/admin/templates/analytics.html +0 -61
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -57
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -126
- package/plugins/contacts/admin/views/contacts.js +0 -710
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -71
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/contacts/plugin.js +0 -291
- package/plugins/contacts/plugin.json +0 -33
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/templates/editor.html +0 -129
- package/plugins/invoice/admin/templates/index.html +0 -43
- package/plugins/invoice/admin/templates/issuers.html +0 -5
- package/plugins/invoice/admin/templates/receivers.html +0 -5
- package/plugins/invoice/admin/views/editor.js +0 -267
- package/plugins/invoice/admin/views/index.js +0 -155
- package/plugins/invoice/admin/views/issuers.js +0 -23
- package/plugins/invoice/admin/views/party-view.js +0 -148
- package/plugins/invoice/admin/views/receivers.js +0 -22
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
- package/plugins/invoice/collections/invoices/schema.json +0 -27
- package/plugins/invoice/config.js +0 -16
- package/plugins/invoice/plugin.js +0 -283
- package/plugins/invoice/plugin.json +0 -85
- package/plugins/invoice/templates/invoice-print.html +0 -213
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/data/notes.json +0 -1
- package/plugins/notes/plugin.json +0 -33
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/data/todos.json +0 -1
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
|
@@ -24,12 +24,47 @@ import {
|
|
|
24
24
|
registerTransform
|
|
25
25
|
} from './hooks.js';
|
|
26
26
|
import {registerPluginResource, unregisterPluginResourcesByPlugin} from './permissionRegistry.js';
|
|
27
|
+
import {classifyEntitlement, mayLoad} from './pluginEntitlement.js';
|
|
27
28
|
import {createCollection, getCollection} from './collections.js';
|
|
28
29
|
|
|
29
30
|
const PLUGINS_DIR = path.resolve('plugins');
|
|
30
31
|
|
|
31
32
|
const REQUIRED_MANIFEST_FIELDS = ['name', 'displayName', 'version', 'description', 'author', 'date', 'icon'];
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Validate a candidate plugin directory the way discoverPlugins() would.
|
|
36
|
+
*
|
|
37
|
+
* Exported so an UPGRADE can check what it extracted before swapping it over a
|
|
38
|
+
* working installation. Validating after the swap would mean discovering the
|
|
39
|
+
* new version is broken with the old one already gone.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} dir - absolute directory
|
|
42
|
+
* @param {string} slug
|
|
43
|
+
* @returns {Promise<{ok: boolean, reason?: string, manifest?: object}>}
|
|
44
|
+
*/
|
|
45
|
+
export async function validatePluginDir(dir, slug) {
|
|
46
|
+
let manifest;
|
|
47
|
+
try {
|
|
48
|
+
manifest = JSON.parse(await fs.readFile(path.join(dir, 'plugin.json'), 'utf8'));
|
|
49
|
+
} catch {
|
|
50
|
+
return {ok: false, reason: 'no readable plugin.json'};
|
|
51
|
+
}
|
|
52
|
+
for (const field of REQUIRED_MANIFEST_FIELDS) {
|
|
53
|
+
if (!manifest[field]) return {ok: false, reason: `missing required field "${field}"`};
|
|
54
|
+
}
|
|
55
|
+
if (manifest.name !== slug) {
|
|
56
|
+
return {ok: false, reason: `manifest name is "${manifest.name}", not "${slug}"`};
|
|
57
|
+
}
|
|
58
|
+
for (const file of ['plugin.js', 'config.js']) {
|
|
59
|
+
try {
|
|
60
|
+
await fs.access(path.join(dir, file));
|
|
61
|
+
} catch {
|
|
62
|
+
return {ok: false, reason: `missing required file ${file}`};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {ok: true, manifest};
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
const failedLoads = [];
|
|
34
69
|
|
|
35
70
|
/**
|
|
@@ -60,11 +95,45 @@ export function getPluginLoadFailures() {
|
|
|
60
95
|
*/
|
|
61
96
|
const _loadedPlugins = {};
|
|
62
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Entitlement state per licensed plugin, as of the last registerPlugins().
|
|
100
|
+
* Reported by GET /api/plugins so the screen can explain an absence rather than
|
|
101
|
+
* simply not showing a plugin the operator knows they installed.
|
|
102
|
+
*
|
|
103
|
+
* @type {Object.<string, string>}
|
|
104
|
+
*/
|
|
105
|
+
const _entitlementStates = {};
|
|
106
|
+
|
|
107
|
+
/** Entitlement states that still let a plugin load. Mirrors pluginEntitlement. */
|
|
108
|
+
const ENTITLEMENT_LOADABLE = new Set(['valid', 'grace', 'support-ended', 'not-required']);
|
|
109
|
+
|
|
110
|
+
/** @returns {Object.<string, string>} */
|
|
111
|
+
export function getEntitlementStates() {
|
|
112
|
+
return {..._entitlementStates};
|
|
113
|
+
}
|
|
114
|
+
|
|
63
115
|
/**
|
|
64
116
|
* Core plugins - always loaded regardless of plugins.json enabled state.
|
|
65
|
-
* These are considered first-class CMS features, not optional add-ons
|
|
117
|
+
* These are considered first-class CMS features, not optional add-ons: they
|
|
118
|
+
* ship with the CMS, cannot be switched off, and their sidebar entries file
|
|
119
|
+
* under Tools rather than Plugins.
|
|
120
|
+
*
|
|
121
|
+
* Being in here is the whole mechanism. `enabled` in plugins.json is ignored
|
|
122
|
+
* for these, so a site that had one switched off still gets it on the next
|
|
123
|
+
* start - which is the point of promoting one.
|
|
124
|
+
*
|
|
125
|
+
* It is also a waiting room rather than a destination, and as of 0.67.0 it is
|
|
126
|
+
* EMPTY. site-search sat here until search moved into the CMS proper, and
|
|
127
|
+
* analytics until it did the same - a feature nobody may switch off has no
|
|
128
|
+
* business being discovered, listed and toggled as though it were an add-on.
|
|
129
|
+
*
|
|
130
|
+
* Kept rather than deleted because the mechanism is the right one for the next
|
|
131
|
+
* promotion: a plugin lands here while its replacement is built, then leaves.
|
|
66
132
|
*/
|
|
67
|
-
const CORE_PLUGINS = new Set([
|
|
133
|
+
const CORE_PLUGINS = new Set([]);
|
|
134
|
+
|
|
135
|
+
/** Is this plugin a built-in feature rather than an optional add-on? */
|
|
136
|
+
export function isCorePlugin(name) { return CORE_PLUGINS.has(name); }
|
|
68
137
|
|
|
69
138
|
/**
|
|
70
139
|
* Scan the plugins/ directory and return all valid manifests.
|
|
@@ -187,6 +256,94 @@ export function savePluginState(name, state) {
|
|
|
187
256
|
saveConfig('plugins', states);
|
|
188
257
|
}
|
|
189
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Work out which plugins are pushed aside by others that are loading.
|
|
261
|
+
*
|
|
262
|
+
* A plugin declares `"supersedes": ["other-plugin"]` in its manifest when it
|
|
263
|
+
* replaces another outright - the Pro edition of something that also ships
|
|
264
|
+
* free, for instance. The superseded plugin then registers nothing at all: no
|
|
265
|
+
* routes, no sidebar item, no collections, so the admin shows one of the pair
|
|
266
|
+
* rather than two that do the same job.
|
|
267
|
+
*
|
|
268
|
+
* The claim lives on the REPLACEMENT, never on the plugin being replaced, so
|
|
269
|
+
* a bundled plugin needs no knowledge of the paid one that may one day
|
|
270
|
+
* displace it.
|
|
271
|
+
*
|
|
272
|
+
* ## This is computed every boot and never written to config/plugins.json
|
|
273
|
+
*
|
|
274
|
+
* Persisting it would be a trap. Licences are withdrawn by disabling a plugin
|
|
275
|
+
* and leaving its files in place, so if superseding had written the free
|
|
276
|
+
* plugin off as disabled, a customer whose licence lapsed would lose the
|
|
277
|
+
* replacement AND the thing it replaced, ending up worse off than someone who
|
|
278
|
+
* never paid. Deriving it at load time means the superseded plugin simply
|
|
279
|
+
* comes back on the next restart.
|
|
280
|
+
*
|
|
281
|
+
* @param {object[]} manifests - every discovered manifest
|
|
282
|
+
* @param {Set<string>} activeNames - names that would otherwise load
|
|
283
|
+
* @param {{isCore?: (name: string) => boolean, warn?: (message: string) => void}} [options]
|
|
284
|
+
* @returns {Map<string, string>} superseded plugin name → the plugin displacing it
|
|
285
|
+
*/
|
|
286
|
+
export function resolveSupersessions(manifests, activeNames, options = {}) {
|
|
287
|
+
const {isCore = () => false, warn = () => {}} = options;
|
|
288
|
+
const byName = new Map(manifests.map(m => [m.name, m]));
|
|
289
|
+
|
|
290
|
+
// Collect the claims worth honouring, dropping the nonsensical ones loudly
|
|
291
|
+
// rather than silently - a manifest that claims the wrong thing is a
|
|
292
|
+
// packaging mistake, and a silent no-op makes it very hard to find.
|
|
293
|
+
const claims = new Map();
|
|
294
|
+
for (const manifest of manifests) {
|
|
295
|
+
if (!activeNames.has(manifest.name)) continue;
|
|
296
|
+
const targets = Array.isArray(manifest.supersedes) ? manifest.supersedes : [];
|
|
297
|
+
const kept = [];
|
|
298
|
+
|
|
299
|
+
for (const target of targets) {
|
|
300
|
+
if (target === manifest.name) {
|
|
301
|
+
warn(`"${manifest.name}" lists itself in supersedes - ignored.`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (!byName.has(target) || !activeNames.has(target)) continue;
|
|
305
|
+
if (isCore(target)) {
|
|
306
|
+
warn(`"${manifest.name}" cannot supersede core plugin "${target}" - ignored.`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const rival = byName.get(target);
|
|
310
|
+
if (Array.isArray(rival.supersedes) && rival.supersedes.includes(manifest.name)) {
|
|
311
|
+
warn(`"${manifest.name}" and "${target}" each claim to supersede the other - neither is applied.`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
kept.push(target);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (kept.length) claims.set(manifest.name, kept);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!claims.size) return new Map();
|
|
321
|
+
|
|
322
|
+
// A superseded plugin's own claims must stop counting, or A>B>C would
|
|
323
|
+
// leave C displaced by a B that is not itself loading. Recompute from
|
|
324
|
+
// scratch each pass until the answer stops changing.
|
|
325
|
+
let superseded = new Map();
|
|
326
|
+
for (let pass = 0; pass <= manifests.length; pass++) {
|
|
327
|
+
const next = new Map();
|
|
328
|
+
for (const [claimant, targets] of claims) {
|
|
329
|
+
if (superseded.has(claimant)) continue;
|
|
330
|
+
for (const target of targets) {
|
|
331
|
+
if (!next.has(target)) next.set(target, claimant);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const settled = next.size === superseded.size
|
|
336
|
+
&& [...next].every(([name, by]) => superseded.get(name) === by);
|
|
337
|
+
superseded = next;
|
|
338
|
+
if (settled) return superseded;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Only reachable via a supersede cycle longer than a mutual pair. Refusing
|
|
342
|
+
// to apply any of it keeps the outcome deterministic and visible.
|
|
343
|
+
warn('supersedes declarations form a cycle - none are applied.');
|
|
344
|
+
return new Map();
|
|
345
|
+
}
|
|
346
|
+
|
|
190
347
|
/**
|
|
191
348
|
* Register server-side Fastify plugins for all enabled plugins.
|
|
192
349
|
* Always loads plugin.js as the entry point.
|
|
@@ -198,11 +355,57 @@ export async function registerPlugins(fastify) {
|
|
|
198
355
|
const manifests = await discoverPlugins();
|
|
199
356
|
const states = getPluginStates();
|
|
200
357
|
|
|
358
|
+
// Entitlement is settled BEFORE supersession, not after, and the ordering is
|
|
359
|
+
// load-bearing. `email-pro` supersedes `mail-reader`; if an unentitled
|
|
360
|
+
// email-pro still counted as active it would displace mail-reader and then
|
|
361
|
+
// fail to load itself, leaving a customer whose licence lapsed with NEITHER -
|
|
362
|
+
// worse off than someone who never paid at all.
|
|
363
|
+
for (const manifest of manifests) {
|
|
364
|
+
if (manifest.closedSource !== true) continue;
|
|
365
|
+
const verdict = mayLoad(manifest);
|
|
366
|
+
_entitlementStates[manifest.name] = verdict.state;
|
|
367
|
+
if (!verdict.allowed) {
|
|
368
|
+
fastify.log.warn(`[plugins] "${manifest.name}" not loaded: ${verdict.state} (${verdict.reason})`);
|
|
369
|
+
} else if (verdict.state !== 'valid') {
|
|
370
|
+
fastify.log.info(`[plugins] "${manifest.name}": ${verdict.state} (${verdict.reason})`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const entitled = (name) => _entitlementStates[name] === undefined
|
|
374
|
+
|| ENTITLEMENT_LOADABLE.has(_entitlementStates[name]);
|
|
375
|
+
|
|
376
|
+
// Which plugins would load on their own account, before anything displaces
|
|
377
|
+
// one another.
|
|
378
|
+
const activeNames = new Set(
|
|
379
|
+
manifests
|
|
380
|
+
.filter(m => (states[m.name]?.enabled || CORE_PLUGINS.has(m.name)) && entitled(m.name))
|
|
381
|
+
.map(m => m.name)
|
|
382
|
+
);
|
|
383
|
+
const superseded = resolveSupersessions(manifests, activeNames, {
|
|
384
|
+
isCore: name => CORE_PLUGINS.has(name),
|
|
385
|
+
warn: message => fastify.log.warn(`[plugins] ${message}`)
|
|
386
|
+
});
|
|
387
|
+
|
|
201
388
|
const loaded = [];
|
|
202
389
|
for (const manifest of manifests) {
|
|
203
390
|
const state = states[manifest.name] || {};
|
|
204
391
|
if (!state.enabled && !CORE_PLUGINS.has(manifest.name)) continue;
|
|
205
392
|
|
|
393
|
+
const displacedBy = superseded.get(manifest.name);
|
|
394
|
+
if (displacedBy) {
|
|
395
|
+
// Recorded rather than merely skipped: the admin Plugins screen
|
|
396
|
+
// needs to say WHY this one is inactive, or the first thing anyone
|
|
397
|
+
// does is toggle it back on and watch nothing happen.
|
|
398
|
+
_loadedPlugins[manifest.name] = {enabled: false, publicEntry: null, supersededBy: displacedBy};
|
|
399
|
+
fastify.log.info(`[plugins] "${manifest.name}" superseded by "${displacedBy}" - not loaded.`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Already classified above, before supersession was resolved. A refusal
|
|
404
|
+
// is NOT written into plugins.json: a lapse recorded as stored state
|
|
405
|
+
// would mean a renewal could not simply restore it, the same reasoning
|
|
406
|
+
// resolveSupersessions() uses for a superseded plugin.
|
|
407
|
+
if (!entitled(manifest.name)) continue;
|
|
408
|
+
|
|
206
409
|
const entryPath = path.join(PLUGINS_DIR, manifest.name, 'plugin.js');
|
|
207
410
|
try {
|
|
208
411
|
const { default: plugin } = await import(entryPath);
|
|
@@ -613,6 +816,98 @@ export async function runLifecycleHook(name, hook, fastify) {
|
|
|
613
816
|
*
|
|
614
817
|
* @returns {Promise<{ sidebar: object[], routes: object[], views: object, css: object[] }>}
|
|
615
818
|
*/
|
|
819
|
+
/**
|
|
820
|
+
* The newest mtime anywhere under a directory of browser code.
|
|
821
|
+
*
|
|
822
|
+
* Browser code that is imported rather than routed has no version of its own
|
|
823
|
+
* - it is fetched under whatever token the importing entry carried. Folding
|
|
824
|
+
* the newest mtime of the whole tree into that token is what makes a change
|
|
825
|
+
* to an imported file actually reach a browser. It applies to the shared
|
|
826
|
+
* plugins/_lib/admin as much as to a plugin's own admin/lib, and missing
|
|
827
|
+
* either presents the same way: a fix that is on disk and not in the tab.
|
|
828
|
+
*
|
|
829
|
+
* @param {string} root absolute path; a missing directory is not an error
|
|
830
|
+
* @returns {Promise<number>} epoch milliseconds, or 0 if there is nothing there
|
|
831
|
+
*/
|
|
832
|
+
async function newestAdminMtime(root) {
|
|
833
|
+
let newest = 0;
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* @param {string} dir
|
|
837
|
+
* @returns {Promise<void>}
|
|
838
|
+
*/
|
|
839
|
+
async function walk(dir) {
|
|
840
|
+
let entries;
|
|
841
|
+
try {
|
|
842
|
+
entries = await fs.readdir(dir, {withFileTypes: true});
|
|
843
|
+
} catch {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
for (const entry of entries) {
|
|
847
|
+
const full = path.join(dir, entry.name);
|
|
848
|
+
if (entry.isDirectory()) {
|
|
849
|
+
await walk(full);
|
|
850
|
+
} else {
|
|
851
|
+
try {
|
|
852
|
+
newest = Math.max(newest, Math.floor((await fs.stat(full)).mtimeMs));
|
|
853
|
+
} catch { /* unreadable - ignore */ }
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
await walk(root);
|
|
859
|
+
return newest;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* The licence line shown on a plugin's admin banner.
|
|
864
|
+
*
|
|
865
|
+
* A manifest may state it outright; otherwise it is derived, because every
|
|
866
|
+
* plugin needs a truthful answer here and almost none of them will ever carry
|
|
867
|
+
* the field. Closed source means it was bought, and the paid plugins are the
|
|
868
|
+
* ones already marked that way for the source-code guard - so the flag that
|
|
869
|
+
* protects the source also names the licence, rather than a second list
|
|
870
|
+
* someone has to remember to add to.
|
|
871
|
+
*
|
|
872
|
+
* @param {object} manifest
|
|
873
|
+
* @returns {string}
|
|
874
|
+
*/
|
|
875
|
+
export function pluginLicence(manifest) {
|
|
876
|
+
const stated = manifest.licence || manifest.license;
|
|
877
|
+
if (typeof stated === 'string' && stated.trim()) return stated.trim();
|
|
878
|
+
if (CORE_PLUGINS.has(manifest.name)) return 'Built in';
|
|
879
|
+
if (manifest.closedSource) return 'Commercial licence';
|
|
880
|
+
return 'Included with Domma CMS';
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* The expiry shown beside the licence on a plugin's admin banner, or null.
|
|
885
|
+
*
|
|
886
|
+
* Only a licensed plugin has one, and what "expires" means depends on how it
|
|
887
|
+
* was bought (see pluginEntitlement.js): a rental STOPS at its end date, a
|
|
888
|
+
* perpetual licence never does and only its support lapses. The banner says
|
|
889
|
+
* which, because "Expires" on something bought outright would frighten a
|
|
890
|
+
* customer about a thing that is not going to happen.
|
|
891
|
+
*
|
|
892
|
+
* An owner wildcard, or a perpetual licence with no support date, has nothing
|
|
893
|
+
* to show and returns null rather than a chip with no date in it.
|
|
894
|
+
*
|
|
895
|
+
* @param {object} manifest
|
|
896
|
+
* @param {object|null} [given] - injectable for testing; read from disk when omitted
|
|
897
|
+
* @returns {{kind: 'rental'|'support', date: string, state: string}|null}
|
|
898
|
+
*/
|
|
899
|
+
export function pluginExpiry(manifest, given) {
|
|
900
|
+
if (manifest?.closedSource !== true) return null;
|
|
901
|
+
const {state, entitlement} = classifyEntitlement(manifest.name, given);
|
|
902
|
+
if (!entitlement || state === 'unlicensed') return null;
|
|
903
|
+
if (entitlement.model === 'rental') {
|
|
904
|
+
return entitlement.expiresAt ? {kind: 'rental', date: entitlement.expiresAt, state} : null;
|
|
905
|
+
}
|
|
906
|
+
return entitlement.supportExpiresAt
|
|
907
|
+
? {kind: 'support', date: entitlement.supportExpiresAt, state}
|
|
908
|
+
: null;
|
|
909
|
+
}
|
|
910
|
+
|
|
616
911
|
export async function getAdminPluginConfig() {
|
|
617
912
|
const manifests = await discoverPlugins();
|
|
618
913
|
const states = getPluginStates();
|
|
@@ -621,6 +916,10 @@ export async function getAdminPluginConfig() {
|
|
|
621
916
|
const routes = [];
|
|
622
917
|
const views = {};
|
|
623
918
|
const css = [];
|
|
919
|
+
// Per-view plugin identity, so the admin can put the same banner above
|
|
920
|
+
// every plugin screen without each plugin having to render one (and each
|
|
921
|
+
// rendering it differently, which is what it did before).
|
|
922
|
+
const meta = {};
|
|
624
923
|
|
|
625
924
|
for (const manifest of manifests) {
|
|
626
925
|
const state = states[manifest.name] || {};
|
|
@@ -629,27 +928,61 @@ export async function getAdminPluginConfig() {
|
|
|
629
928
|
if (manifest.admin.sidebar) sidebar.push(...manifest.admin.sidebar);
|
|
630
929
|
if (manifest.admin.routes) routes.push(...manifest.admin.routes);
|
|
631
930
|
|
|
632
|
-
// Auto-stamp view entries with mtime-based version
|
|
931
|
+
// Auto-stamp view entries with an mtime-based version.
|
|
932
|
+
//
|
|
933
|
+
// The stamp is the NEWEST mtime of the entry, the plugin's own admin
|
|
934
|
+
// tree and the shared admin library - not the entry alone. A view is
|
|
935
|
+
// usually a thin wrapper that rarely changes, while the code it
|
|
936
|
+
// imports changes constantly, so stamping only the entry left the
|
|
937
|
+
// browser fetching the same URL and serving the previous build of
|
|
938
|
+
// everything behind it. That presents as a feature that worked
|
|
939
|
+
// yesterday and does not today, on one machine.
|
|
940
|
+
// Once per plugin, not per view: it verifies a signature.
|
|
941
|
+
const expiry = pluginExpiry(manifest);
|
|
942
|
+
const sharedStamp = Math.max(
|
|
943
|
+
await newestAdminMtime(path.join(PLUGINS_DIR, '_lib', 'admin')),
|
|
944
|
+
await newestAdminMtime(path.join(PLUGINS_DIR, manifest.name, 'admin'))
|
|
945
|
+
);
|
|
633
946
|
for (const [viewName, viewDef] of Object.entries(manifest.admin.views || {})) {
|
|
634
947
|
const entryBase = viewDef.entry.split('?')[0];
|
|
635
948
|
let stamped = viewDef.entry;
|
|
636
949
|
try {
|
|
637
950
|
const filePath = path.join(PLUGINS_DIR, entryBase);
|
|
638
951
|
const s = await fs.stat(filePath);
|
|
639
|
-
stamped = `${entryBase}?v=${Math.floor(s.mtimeMs)}`;
|
|
952
|
+
stamped = `${entryBase}?v=${Math.max(Math.floor(s.mtimeMs), sharedStamp)}`;
|
|
640
953
|
} catch { /* keep original entry as-is */ }
|
|
641
954
|
views[viewName] = { ...viewDef, entry: stamped };
|
|
955
|
+
meta[viewName] = {
|
|
956
|
+
plugin: manifest.name,
|
|
957
|
+
displayName: manifest.displayName || manifest.name,
|
|
958
|
+
version: manifest.version || '1.0.0',
|
|
959
|
+
date: manifest.date || '',
|
|
960
|
+
author: manifest.author || '',
|
|
961
|
+
icon: manifest.icon || 'package',
|
|
962
|
+
core: CORE_PLUGINS.has(manifest.name),
|
|
963
|
+
licence: pluginLicence(manifest),
|
|
964
|
+
expiry
|
|
965
|
+
};
|
|
642
966
|
}
|
|
643
967
|
|
|
644
|
-
// Collect admin CSS links declared in plugin.json admin.css[]
|
|
968
|
+
// Collect admin CSS links declared in plugin.json admin.css[], stamped
|
|
969
|
+
// with the file's mtime for the same reason view entries are: a browser
|
|
970
|
+
// keys its cache on the full URL, so an unstamped stylesheet keeps
|
|
971
|
+
// serving the old rules after an update and the plugin looks broken in
|
|
972
|
+
// a way that only a hard refresh explains.
|
|
645
973
|
for (let i = 0; i < (manifest.admin.css || []).length; i++) {
|
|
646
974
|
const cssPath = manifest.admin.css[i];
|
|
975
|
+
let href = `/plugins/${manifest.name}/${cssPath}`;
|
|
976
|
+
try {
|
|
977
|
+
const stat = await fs.stat(path.join(PLUGINS_DIR, manifest.name, cssPath));
|
|
978
|
+
href += `?v=${Math.max(Math.floor(stat.mtimeMs), sharedStamp)}`;
|
|
979
|
+
} catch { /* unreadable - link it unstamped rather than not at all */ }
|
|
647
980
|
css.push({
|
|
648
981
|
id: `plugin-${manifest.name}-css${i > 0 ? `-${i}` : ''}`,
|
|
649
|
-
href
|
|
982
|
+
href
|
|
650
983
|
});
|
|
651
984
|
}
|
|
652
985
|
}
|
|
653
986
|
|
|
654
|
-
return { sidebar, routes, views, css };
|
|
987
|
+
return { sidebar, routes, views, css, meta };
|
|
655
988
|
}
|