domma-cms 1.8.46 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/admin/js/lib/shortcode-registry.js +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +1 -1
- package/admin/js/templates/docs/usage-users-roles.html +4 -0
- package/bin/lib/left-free-tier.js +83 -0
- package/bin/update.js +24 -0
- package/docs/DommaFeatures.md +47 -7
- package/docs/markdown-shortcodes.md +4 -0
- package/docs/plugin-development.md +7 -0
- package/package.json +1 -1
- package/plugins/_lib/games/names.js +6 -0
- package/plugins/_lib/public/games/motion.css +1 -0
- package/plugins/_lib/public/games/motion.js +28 -0
- package/plugins/free-tier.lock.json +1 -43
- package/server/routes/api/api-endpoints.js +5 -1
- package/server/routes/api/collections.js +4 -3
- package/server/routes/api/forms.js +22 -0
- package/server/routes/api/views.js +18 -0
- package/server/services/collectionAccess.js +97 -15
- package/server/services/markdown.js +7 -1
- package/server/services/permissionRegistry.js +39 -11
- package/plugins/crossword/CLAUDE.md +0 -37
- package/plugins/crossword/admin/css/index.css +0 -18
- package/plugins/crossword/admin/templates/crossword.html +0 -1
- package/plugins/crossword/admin/views/crossword.js +0 -449
- package/plugins/crossword/collections/crossword-plays.json +0 -20
- package/plugins/crossword/collections/crossword-puzzles.json +0 -17
- package/plugins/crossword/config.js +0 -6
- package/plugins/crossword/docs/guide.md +0 -53
- package/plugins/crossword/inject/body.html +0 -1
- package/plugins/crossword/lib/crossword.js +0 -170
- package/plugins/crossword/lib/store.js +0 -56
- package/plugins/crossword/plugin.js +0 -274
- package/plugins/crossword/plugin.json +0 -79
- package/plugins/crossword/public/crossword.css +0 -36
- package/plugins/crossword/public/crossword.js +0 -519
- package/plugins/crossword/public/lib/layout.js +0 -199
- package/plugins/quiz/CLAUDE.md +0 -43
- package/plugins/quiz/admin/css/index.css +0 -13
- package/plugins/quiz/admin/templates/quiz.html +0 -1
- package/plugins/quiz/admin/views/quiz.js +0 -530
- package/plugins/quiz/collections/quiz-plays.json +0 -20
- package/plugins/quiz/collections/quiz-quizzes.json +0 -17
- package/plugins/quiz/config.js +0 -6
- package/plugins/quiz/docs/guide.md +0 -61
- package/plugins/quiz/inject/body.html +0 -1
- package/plugins/quiz/lib/engine.js +0 -115
- package/plugins/quiz/lib/quiz.js +0 -225
- package/plugins/quiz/lib/store.js +0 -56
- package/plugins/quiz/plugin.js +0 -275
- package/plugins/quiz/plugin.json +0 -79
- package/plugins/quiz/public/quiz.css +0 -36
- package/plugins/quiz/public/quiz.js +0 -372
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.9.1] - 2026-10-24
|
|
4
|
+
|
|
5
|
+
- Columns support content alignment with [col left], [col center] / [col centre], [col right] or align="...". The Page editor offers a Content alignment selector for columns.
|
|
6
|
+
|
|
7
|
+
## [1.9.0] - 2026-10-23
|
|
8
|
+
|
|
9
|
+
- **Security:** a plugin's collections are no longer open through the generic Collections API to someone who holds only part of the plugin's permission. Before, `collections` plus any action of any of the plugin's permissions (a game's View, say) was enough to list every row - quiz answers, puzzle solutions, future daily words and players' play tokens. Now the caller needs the action the permission names as its `collectionAction` in `plugin.json` (the games name `manage`), or, when it names none, every action of that permission. The same check now guards the other ways in: a form named after or pointed at a plugin's collection, a View reading or joining one, and an API Builder endpoint exposing one. A switched-off plugin's collections keep the gate its `plugin.json` declares. Super Admin keeps everything, and a plugin with no permissions still needs only `collections`.
|
|
10
|
+
- **Two new Games, and more in every Game.** Word Search and Daily Word join Quiz and Crossword under Tools > Games, all four Pro Tools that can be gifted. Every Game now has hints for a time penalty, a personal best and a Share button on the finished screen, `board="show|hide|only"` on its shortcode, `[games-leaderboards]` for every Game's boards on one page, and animations (a setting, never for visitors who ask for less motion). The engine carries the Games' shared animations (`plugins/_lib/public/games/motion.css` and `motion.js`); Quiz 1.4.0, Crossword 1.4.0, Word Search 1.1.0 and Daily Word 1.2.0 need this release.
|
|
11
|
+
- **Quiz and Crossword are Pro Tools now, and can be gifted.** They no longer ship inside the engine: a site gets them with a licence, or as a gift from Domma for good or for a number of days. Updating removes the old free copy from a site that is not licensed for it; its data stays, and a licensed or gifted copy is never touched. Quiz and Crossword 1.3.0.
|
|
12
|
+
- **Export and import quizzes and crosswords.** Export one from its menu or its editor, or every one with **Export all**, as a file; **Import…** adds a file's quizzes or crosswords to another site (or the same one). They arrive as drafts with short names of their own, so nothing goes live or changes by surprise; anything that is not valid is skipped and named. Plays and leaderboards stay where they are. Crossword grids come out the same, laid out again from the words. Quiz and Crossword 1.2.0.
|
|
13
|
+
|
|
3
14
|
## [1.8.46] - 2026-10-23
|
|
4
15
|
|
|
5
16
|
- **Format lays HTML out properly.** Shift+Alt+F used to re-indent HTML line by line and nothing more, so `<div><h2>…</h2><p>…</p></div>` stayed on one line and a closer trailing a line threw the rest out. It now puts every block element (div, p, li, h2, section, custom elements…) on a line of its own, indented by nesting, with `{{#if}}` sections the same; text and inline tags (`<a>`, `<strong>`, `<span>`) keep the lines they were written on, so no space appears or goes on the page. Tags get one space between attributes, a tag wider than 120 columns an attribute a line. HTML blocks in the page body are laid out too (new setting *Lay out HTML in the page body*, on), and Enter after an opening block tag in the body goes one deeper. Checked across the fleet's pages (all render the same) and the CMS's own 150 templates.
|
|
@@ -18,7 +18,7 @@ Right
|
|
|
18
18
|
[/col]
|
|
19
19
|
[/row]`,children:{name:"col",min:2,max:6},attrs:[{name:"gap",type:"number",label:"Gap"},{name:"class",type:"text",label:"CSS class"},{name:"id",type:"text",label:"ID"},{name:"reveal",type:"checkbox",label:"Scroll reveal"},{name:"reveal-mode",type:"select",label:"Reveal mode",options:[["stagger","Stagger"],["sequence","Sequence"]]},{name:"reveal-animation",type:"select",label:"Reveal animation",options:[["slide-up","Slide up"],["slide-down","Slide down"],["slide-left","Slide left"],["slide-right","Slide right"],["fade","Fade"],["zoom","Zoom"],["flip","Flip"]]},{name:"reveal-duration",type:"number",label:"Reveal duration (ms)"},{name:"reveal-stagger",type:"number",label:"Reveal stagger (ms)"},{name:"reveal-delay",type:"number",label:"Reveal delay (ms)"},{name:"reveal-direction",type:"select",label:"Reveal direction",options:[["ltr","Left to right"],["rtl","Right to left"]]}]},{name:"col",label:"Column",icon:"columns",category:"Layout",child:!0,selfClosing:!1,container:!0,wrappable:!0,template:`[col]
|
|
20
20
|
Content
|
|
21
|
-
[/col]`,attrs:[{name:"span",type:"number",label:"Span"},{name:"id",type:"text",label:"ID"}]},{name:"slideover",label:"Slideover",icon:"panel-right",category:"Layout",selfClosing:!1,container:!0,wrappable:!0,template:`[slideover title="More information" trigger="Read more"]
|
|
21
|
+
[/col]`,attrs:[{name:"span",type:"number",label:"Span"},{name:"align",type:"select",label:"Content alignment",options:[["","Inherit"],["left","Left"],["center","Centre"],["centre","Centre (alias)"],["right","Right"]]},{name:"id",type:"text",label:"ID"}]},{name:"slideover",label:"Slideover",icon:"panel-right",category:"Layout",selfClosing:!1,container:!0,wrappable:!0,template:`[slideover title="More information" trigger="Read more"]
|
|
22
22
|
Content
|
|
23
23
|
[/slideover]`,attrs:[{name:"title",type:"text",label:"Title"},{name:"trigger",type:"text",label:"Trigger label"},{name:"size",type:"select",label:"Size",options:[["sm","Small"],["md","Medium"],["lg","Large"]]},{name:"position",type:"select",label:"Position",options:[["right","Right"],["left","Left"]]}]},{name:"center",label:"Centre",icon:"text-center",category:"Layout",selfClosing:!1,container:!0,wrappable:!0,template:`[center]
|
|
24
24
|
Centred content
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
</tr>
|
|
32
32
|
<tr>
|
|
33
33
|
<td><code>[col span="N"]...[/col]</code></td>
|
|
34
|
-
<td>Column inside a grid or row; <code>span</code> sets column span</td>
|
|
34
|
+
<td>Column inside a grid or row; <code>span</code> sets column span. Use <code>align="left|center|centre|right"</code> or a flag such as <code>[col centre]</code> to align content</td>
|
|
35
35
|
</tr>
|
|
36
36
|
</tbody>
|
|
37
37
|
</table>
|
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
and Blog Editor, for example). They appear on the Roles screen
|
|
63
63
|
like any other and you can edit them; the plugin never overwrites your changes.
|
|
64
64
|
Switching the plugin off keeps its roles; uninstalling it removes them and moves their users to <code>user</code>.</li>
|
|
65
|
+
<li><strong>Plugin data in Collections.</strong> The Collections screen, form submissions,
|
|
66
|
+
Views and API Builder open a plugin's own collections only for someone who can fully manage
|
|
67
|
+
that plugin (for the games, their Manage permission). Someone who may only view a game
|
|
68
|
+
cannot read its answers or players' rows through any of them.</li>
|
|
65
69
|
</ul>
|
|
66
70
|
|
|
67
71
|
<h3>Confining a role to some screens</h3>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools that left the engine's free tier, for the engine updater.
|
|
3
|
+
*
|
|
4
|
+
* Quiz and Crossword shipped free inside the engine until 1.3.0, when they
|
|
5
|
+
* became paid (and giftable) Tools that reach a site through the catalogue and
|
|
6
|
+
* a licence. The engine stops shipping them, but an update only ever touches
|
|
7
|
+
* the plugins it ships - so without this step every site would keep its old
|
|
8
|
+
* free copy forever, runnable without a licence.
|
|
9
|
+
*
|
|
10
|
+
* So the update removes the OLD FREE COPY, and only that:
|
|
11
|
+
*
|
|
12
|
+
* - a copy with `.entitlement.json` beside it is a licensed (or gifted) one,
|
|
13
|
+
* installed the paid way: kept, untouched;
|
|
14
|
+
* - a copy whose plugin.json says it is paid (`closedSource` or `tier: paid`)
|
|
15
|
+
* is the new Tool already - the Manager runs it on its owner entitlement,
|
|
16
|
+
* with no `.entitlement.json`: kept, untouched;
|
|
17
|
+
* - a copy with no readable plugin.json, or a symlinked folder, is not ours to judge: kept;
|
|
18
|
+
* - otherwise its code goes, but `data/` and `backups/` stay where they are
|
|
19
|
+
* (the loader skips a folder with no plugin.json, and a paid install later
|
|
20
|
+
* keeps them - see the 2026-09-23 incident in the manager's pluginPolicy.js).
|
|
21
|
+
* The folder itself goes only when nothing is left in it.
|
|
22
|
+
*
|
|
23
|
+
* The updater backs up the whole plugins/ folder before this runs (unless
|
|
24
|
+
* --no-backup).
|
|
25
|
+
*
|
|
26
|
+
* @module bin/lib/left-free-tier
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {existsSync, lstatSync, readFileSync, readdirSync, rmSync} from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
|
|
32
|
+
/** Slug -> the version that left the free tier. */
|
|
33
|
+
export const LEFT_FREE_TIER = Object.freeze({quiz: '1.3.0', crossword: '1.3.0'});
|
|
34
|
+
|
|
35
|
+
/** Never removed from a copy that goes: runtime state, and a licence (which would have kept the copy anyway). */
|
|
36
|
+
export const KEEP = Object.freeze(['data', 'backups', '.entitlement.json']);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What to do with one site's copy of a plugin that left the free tier.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} dir - the site's plugins/<slug>
|
|
42
|
+
* @returns {{action: 'none'|'keep'|'remove', reason: string}}
|
|
43
|
+
*/
|
|
44
|
+
export function judgeCopy(dir) {
|
|
45
|
+
if (!existsSync(dir)) return {action: 'none', reason: 'not installed'};
|
|
46
|
+
// A linked folder (a dev checkout pointing at a source tree) is never ours to empty.
|
|
47
|
+
if (lstatSync(dir).isSymbolicLink()) return {action: 'keep', reason: 'a symlink'};
|
|
48
|
+
if (existsSync(path.join(dir, '.entitlement.json'))) return {action: 'keep', reason: 'licensed (.entitlement.json)'};
|
|
49
|
+
let manifest;
|
|
50
|
+
try {
|
|
51
|
+
manifest = JSON.parse(readFileSync(path.join(dir, 'plugin.json'), 'utf8'));
|
|
52
|
+
} catch {
|
|
53
|
+
return {action: 'keep', reason: 'no readable plugin.json'};
|
|
54
|
+
}
|
|
55
|
+
if (manifest.closedSource === true || manifest.tier === 'paid') return {action: 'keep', reason: `the paid Tool (${manifest.version || '?'})`};
|
|
56
|
+
return {action: 'remove', reason: `the old free copy (${manifest.version || '?'})`};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Remove the old free copies of every plugin that left the free tier.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} pluginsDir - the site's plugins/
|
|
63
|
+
* @param {{names?: string[], dryRun?: boolean}} [opts]
|
|
64
|
+
* @returns {{name: string, action: 'keep'|'remove', reason: string, kept: string[]}[]} one row per copy found
|
|
65
|
+
*/
|
|
66
|
+
export function removeLeftFreeTier(pluginsDir, {names = Object.keys(LEFT_FREE_TIER), dryRun = false} = {}) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const name of names) {
|
|
69
|
+
const dir = path.join(pluginsDir, name);
|
|
70
|
+
const {action, reason} = judgeCopy(dir);
|
|
71
|
+
if (action === 'none') continue;
|
|
72
|
+
const kept = [];
|
|
73
|
+
if (action === 'remove') {
|
|
74
|
+
for (const entry of readdirSync(dir)) {
|
|
75
|
+
if (KEEP.includes(entry)) { kept.push(entry); continue; }
|
|
76
|
+
if (!dryRun) rmSync(path.join(dir, entry), {recursive: true, force: true});
|
|
77
|
+
}
|
|
78
|
+
if (!dryRun && !kept.length) rmSync(dir, {recursive: true, force: true});
|
|
79
|
+
}
|
|
80
|
+
out.push({name, action, reason, kept});
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
package/bin/update.js
CHANGED
|
@@ -16,6 +16,7 @@ import {fileURLToPath} from 'node:url';
|
|
|
16
16
|
import {deepMergeNewKeys} from './lib/config-merge.js';
|
|
17
17
|
import {shouldReplacePlugin} from './lib/plugin-version.js';
|
|
18
18
|
import {KEEP_UPDATE_BACKUPS, pruneUpdateBackups} from './lib/backup-retention.js';
|
|
19
|
+
import {removeLeftFreeTier} from './lib/left-free-tier.js';
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Paths
|
|
@@ -249,6 +250,11 @@ export default async function update(_positional, flags) {
|
|
|
249
250
|
info(` → plugins/${name}/ (${exists ? 'update' : 'add'})`);
|
|
250
251
|
}
|
|
251
252
|
}
|
|
253
|
+
for (const r of removeLeftFreeTier(path.join(cwd, 'plugins'), {dryRun: true})) {
|
|
254
|
+
info(r.action === 'remove'
|
|
255
|
+
? ` → plugins/${r.name}/ (remove ${r.reason} - it is a paid Tool now${r.kept.length ? `; ${r.kept.join(', ')} kept` : ''})`
|
|
256
|
+
: ` → plugins/${r.name}/ (kept: ${r.reason})`);
|
|
257
|
+
}
|
|
252
258
|
console.log('');
|
|
253
259
|
|
|
254
260
|
info('Package.json: CMS deps would be replaced; user-added deps preserved.');
|
|
@@ -436,6 +442,24 @@ export default async function update(_positional, flags) {
|
|
|
436
442
|
}
|
|
437
443
|
done();
|
|
438
444
|
|
|
445
|
+
// -------------------------------------------------------------------------
|
|
446
|
+
// 7b. Tools that left the free tier (Quiz and Crossword, 1.3.0): the old
|
|
447
|
+
// free copy goes - never a licensed one, never the paid Tool, never
|
|
448
|
+
// its data/ (bin/lib/left-free-tier.js).
|
|
449
|
+
// -------------------------------------------------------------------------
|
|
450
|
+
{
|
|
451
|
+
const left = removeLeftFreeTier(pluginsDirDst);
|
|
452
|
+
if (left.length) {
|
|
453
|
+
let enabled = {};
|
|
454
|
+
try { enabled = readJson(path.join(cwd, 'config/plugins.json')); } catch { /* none */ }
|
|
455
|
+
for (const r of left) {
|
|
456
|
+
if (r.action === 'keep') { info(` → plugins/${r.name}/ kept: ${r.reason}`); continue; }
|
|
457
|
+
info(` → plugins/${r.name}/ removed: ${r.reason} - it is a paid Tool now${r.kept.length ? ` (${r.kept.join(', ')} kept)` : ''}`);
|
|
458
|
+
if (enabled[r.name]?.enabled) warn(`${r.name} was switched on here: it stays off until this site is licensed for it (or gifted it).`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
439
463
|
// -------------------------------------------------------------------------
|
|
440
464
|
// 8. Register new plugins in plugins.json (disabled by default - except the
|
|
441
465
|
// ones that protect the site, which arrive switched on: a security tool
|
package/docs/DommaFeatures.md
CHANGED
|
@@ -23,7 +23,7 @@ Code was treated as the truth. Where docs disagree with it, the disagreement is
|
|
|
23
23
|
- **Operations** - Node 24, `npx` scaffolder and CLI, an updater that merges config, fleet management through
|
|
24
24
|
domma-cms-manager, response cache, built-in security hardening.
|
|
25
25
|
- **Marketplace** - signed `.dcmsplugin` packages, licensed paid plugins, safe upgrades with rollback,
|
|
26
|
-
`minCmsVersion` checks, user guides in the admin's Documentation folder, and the
|
|
26
|
+
`minCmsVersion` checks, user guides in the admin's Documentation folder, and the plugins below.
|
|
27
27
|
|
|
28
28
|
## Marketplace catalogue
|
|
29
29
|
|
|
@@ -38,7 +38,8 @@ Code was treated as the truth. Where docs disagree with it, the disagreement is
|
|
|
38
38
|
| `calendar` | paid | 1.15.4 | Events calendar - month, day and list views, online meetings as a video call on your site (with the call's chat kept, and Add people during the call), call anyone now from a contact or an email, all-day and recurring events, sharing with people and roles, RSVPs and an iCal feed. |
|
|
39
39
|
| `campaigns-pro` | paid | 1.0.2 | Marketing campaigns, one paid Tool: compose a message, send it to an audience imported from Contacts (shared with Surveys), schedule sends, attach an offer that closes by a date or a claim limit. Opens and clicks (cookieless), links tagged for Analytics, a linked Shop discount code, send a survey as a campaign, follow-up sequences, resend to non-openers, A/B subject tests, and a version-1 stats feed other Tools read. |
|
|
40
40
|
| `contacts-pro` | paid | 0.12.0 | Contacts, with more to them: your own custom fields, birthdays and ages with the ones coming up and a reminder in the sidebar, and groups with their own emblem and colours. Share a contact, several, or a whole group with people or roles - to view, or to edit. Call a contact now on a video call on your site. Works over the same contacts as the built-in Tool, so nothing moves and nothing is lost. |
|
|
41
|
-
| `crossword` |
|
|
41
|
+
| `crossword` | paid (giftable) | 1.4.0 | Crosswords on your site: type the answers and clues and the grid lays itself out (live preview, Another layout). Players type a name and race the server's clock, checks and wrong guesses cost time, fastest solvers on a leaderboard. Hints for a time penalty, personal best and Share, animations, export and import. Test it from the Tool - a test go never reaches the board. |
|
|
42
|
+
| `daily-word` | paid (giftable) | 1.2.0 | One hidden word a day, the same for everyone: six guesses, each letter marked right place, in the word or not; real words only (a bundled dictionary, switchable), an archive of past days, streaks, Share as coloured squares, a calendar of what is coming. Fewest guesses tops the day's board, then the fastest. |
|
|
42
43
|
| `data-transfer` | paid | 1.3.3 | Move collections between file and MongoDB storage, back them up and restore them (all or some, merged or replaced) - keeping every entry's id - and clone them (cloned entries get new ids, so references stay unambiguous). Every restore and transfer takes a safety snapshot first. |
|
|
43
44
|
| `docs` | paid | 2.13.1 | Document editor: Home and editor screens, sharing with Viewers/Editors, public links, import/export, send to page/blog/email, edit lock, Trash, version history, templates. PDF documents (2.13): a PDF kept as it is, shown in the browser's viewer, searchable inside, versioned on replace. |
|
|
44
45
|
| `docs-pro` | paid | 0.11.0 | Domma Spaces (publish a Docs folder at `/spaces` with a Kindle-style reader), Cloud copies (Google Drive, OneDrive, Dropbox, S3), Signatures (emailed signing links, typed or drawn, signed copy with a certificate page; 0.10: boxes placed on a PDF document's pages, the signed PDF stamped with pdf-lib), PDF tools (0.11: fill the PDF's own form, annotate, pages) and protected public links (expiry date, password). Adds to Docs. |
|
|
@@ -51,7 +52,7 @@ Code was treated as the truth. Where docs disagree with it, the disagreement is
|
|
|
51
52
|
| `messages` | paid | 1.7.2 | Internal user-to-user and group messaging with real-time delivery and video calls (what is typed in a call lands in the conversation; ring another team member into a call) - conversations you can archive, mark unread and search, drafts kept per conversation, attachments, and an unread badge in the sidebar. |
|
|
52
53
|
| `mongodb-adapter` | paid | 1.0.2 | Provision a dedicated MongoDB database per site. Credentials are managed centrally by the manager. |
|
|
53
54
|
| `onboard-pro` | paid | 1.0.1 | Onboarding journeys, one paid Tool: read/form/details/manual steps, a personal link per person, signature steps through Docs Pro, admin-approved account steps (never an administrator), due dates, automatic nudges and auto-enrolment from a contact group, countersigned steps, waive-with-a-reason, and a funnel showing where people stop. |
|
|
54
|
-
| `quiz` |
|
|
55
|
+
| `quiz` | paid (giftable) | 1.4.0 | Timed quizzes on your site: one or several right answers, true or false, or a typed answer, marked by the server so answers never reach the browser early. Name-only players, leaderboards by day, week, month and all time. Hints for a time penalty, personal best and Share, animations, export and import. Test it from the Tool - a test go never reaches the board. |
|
|
55
56
|
| `security` | free | 1.1.3 | Keeps a Domma CMS site safe: a health check of what is weak and how to fix it, sign-in lockout after repeated wrong passwords, password rules, a log of every sign-in, and blocking of the scanners that probe every site for WordPress, .env files and the like. |
|
|
56
57
|
| `security-pro` | paid | 1.3.2 | Everything in Security, plus two-factor sign-in (authenticator apps, recovery codes, required per role), a firewall that bans scanners for longer each time they return, per-route rate limits, country rules for the admin, breached-password checks and every signed-in session in one place. |
|
|
57
58
|
| `seo-pro` | paid | 0.6.2 | More for SEO: Google Search Console in the admin (with your own Google client) - searches, clicks, positions and where to win more; structured data for rich results (FAQ, how-to, product, event, local business); links worth adding between pages, pages competing for one search, readability, near duplicates and image checks; every title and description in one grid, and moving an old site's addresses across as redirects; a not-found log with one-click redirects; a scheduled check of every link; SEO history and notices. Adds to SEO, which is built into Domma CMS (0.90 and later). |
|
|
@@ -62,6 +63,7 @@ Code was treated as the truth. Where docs disagree with it, the disagreement is
|
|
|
62
63
|
| `surveys` | paid | 1.5.1 | Targeted surveys over the Forms engine: shared audiences, tokenised invites, reminders, and per-survey statistics. |
|
|
63
64
|
| `theme-roller` | paid | 1.1.3 | Make your own themes: start from any built-in theme, change its colours, fonts, spacing and borders with a live preview, and save it. Saved themes join the built-in ones everywhere a theme is chosen - the site, the admin, day and night, a region of a page. Needs Domma CMS 0.73 or later to use them. |
|
|
64
65
|
| `waypoint-pro` | paid | 1.3.1 | Plan and track work: projects with their own workflows, issues on a board or in a list, a backlog planned into sprints with a burndown, and comments, mentions and watchers that tell the right people. |
|
|
66
|
+
| `word-search` | paid (giftable) | 1.1.0 | Word searches on your site: type up to 40 words and the server lays out the letter grid (across, down, diagonal, backwards too if you like). Players drag, tap or use the keyboard to mark each word against the clock; wrong marks cost time, fastest full solve wins. Hints, personal best, Share, animations, export and import. |
|
|
65
67
|
|
|
66
68
|
Versions are read from `dcms-marketplace/plugins/*/plugin.json` (every plugin folder except `_lib`).
|
|
67
69
|
|
|
@@ -1211,6 +1213,8 @@ The four Tools below were plugins until 0.67.0 and are now part of the CMS. They
|
|
|
1211
1213
|
Where: Role editor > Admin area; `normaliseAdminScope` / `getAdminScope` in `roles.js`; `admin/js/lib/admin-scope.js` - Since: 0.71.0.
|
|
1212
1214
|
- **Plugin roles and permissions** - Plugins declare `permissions[]` in `plugin.json`, with `grant` to give them to existing roles, and ship `roles/*.json`. They can also create and remove roles from code with `hooks.registerRole()` and `unregisterRole()`. A plugin role is created once and then belongs to the admin. Disabling the plugin keeps the role, so user assignments survive, and uninstalling removes it.
|
|
1213
1215
|
Since: 0.71.0.
|
|
1216
|
+
- **Plugin collections in the generic Collections API** - A collection a plugin created is reachable through `/api/collections/<slug>/...` only by someone with full use of one of the plugin's permissions: the action that permission names as `collectionAction` in `plugin.json` (Quiz, Crossword, Word Search and Daily Word name `manage`), or every action it has. A read-only holder is refused, so answers, solutions and play tokens are not readable through the Collections API, a form's submissions, Views or API Builder. A switched-off plugin's collections keep the gate its `plugin.json` declares. Super Admin keeps everything; a plugin with no permissions needs only `collections`.
|
|
1217
|
+
Where: `pluginCollectionGates` / `pluginDenial` / `pluginDenialFor` in `server/services/collectionAccess.js`; forms, views and api-endpoints routes - Since: Unreleased.
|
|
1214
1218
|
- **Role-aware public site** - A signed-in visitor sees the navbar, footer and menu items meant for their role. Visibility supports both "this role and above" and `=role` ("this role only").
|
|
1215
1219
|
Since: 0.71.0.
|
|
1216
1220
|
|
|
@@ -1316,7 +1320,7 @@ The four Tools below were plugins until 0.67.0 and are now part of the CMS. They
|
|
|
1316
1320
|
|
|
1317
1321
|
- **Managed sites** - A site forked by the manager gets `MANAGER_URL`, `SITE_SLUG` and a per-fork `FLEET_TOKEN` in its environment. It then:
|
|
1318
1322
|
- browses the fleet catalogue under Marketplace > Browse, with prices, licence state and updates;
|
|
1319
|
-
- installs paid plugins with an entitlement issued for that site;
|
|
1323
|
+
- installs paid plugins with an entitlement issued for that site, sold or gifted (a giftable Tool can be gifted for good or for a number of days - see [Tiers, licences and entitlements](#tiers-licences-and-entitlements));
|
|
1320
1324
|
- receives pushed notifications (`MANAGER_SECRET`).
|
|
1321
1325
|
|
|
1322
1326
|
The manager also writes `tools.json` and `plugins.json` directly to set switches and locks. Every call to the manager fails gracefully, so a manager that is down never stops the site serving pages. Unmanaged sites install plugins from a `.dcmsplugin` file instead.
|
|
@@ -1413,6 +1417,7 @@ Domma CMS is extended by plugins: self-contained folders under `plugins/<slug>/`
|
|
|
1413
1417
|
- **Current register (marketplace, 24 plugins)** - versions in the [catalogue table](#marketplace-catalogue).
|
|
1414
1418
|
- Free (6, shipped with the engine): `blog`, `booking`, `feedback`, `mail-reader`, `security`, `shopping-cart`.
|
|
1415
1419
|
- Paid (21): `analytics-pro`, `blog-pro`, `booking-pro`, `calendar`, `contacts-pro`, `data-transfer`, `docs`, `docs-pro`, `email-pro`, `garage`, `invoice`, `job-board`, `messages`, `mongodb-adapter`, `security-pro`, `seo-pro`, `shopping-cart-pro`, `social-media-pro`, `surveys`, `theme-roller`, `waypoint-pro`.
|
|
1420
|
+
- Paid and giftable (4, the Games): `quiz`, `crossword`, `word-search`, `daily-word`.
|
|
1416
1421
|
- **Free-and-Pro pairs** - A Pro edition declares `supersedes` on its free sibling, so only one runs: Blog Pro → Blog, Email Pro → Mail Reader, Security Pro → Security, Contacts Pro → Contacts (built-in Tool takeover). Shopping Cart Pro and SEO Pro instead `requires` their free base (`shopping-cart`, the `seo` Tool).
|
|
1417
1422
|
- **MongoDB-dependent plugins** - Email Pro ("Requires MongoDB" in its description), Data Transfer (moves collections between file and MongoDB storage) and MongoDB Adapter (managed-only: reads a `config/connections.json` the manager writes during provisioning). There is no manifest field for a Mongo requirement - it is stated only in the description text. Core already supports Mongo storage for free via `storage: {adapter: "mongodb"}` in a schema.
|
|
1418
1423
|
- Where: design spec "Managed-only plugins"
|
|
@@ -1421,6 +1426,8 @@ Domma CMS is extended by plugins: self-contained folders under `plugins/<slug>/`
|
|
|
1421
1426
|
- `perpetual` (self-hosted, bought outright): never stops. After `supportExpiresAt` it is `support-ended` - still runs, updates lapse.
|
|
1422
1427
|
- Absent, wrong slug, unknown key or bad signature → `unlicensed` - does not load.
|
|
1423
1428
|
- Where: `server/services/pluginEntitlement.js` · Since: 0.68.0
|
|
1429
|
+
- **Giftable Tools and gifts** - A paid manifest may add `giftable: true` (the catalogue carries it; refused on a free Tool). The Manager can then gift that Tool to a site at no charge, from the site's Plugins tab or the Plugin Store, **for good** or **for a number of days**. A gift is an ordinary licence with `type: 'gift'`, `giftedBy` and `giftedAt`, no grace: for good is minted perpetual (no expiry), a timed gift is a rental that tells the site's admins three days before and switches the Tool off on its last day. **Extend gift** adds days or makes it for good without a restart; **End gift** is the rescind (disable only, files and data stay). A gift is refused over a live paid licence and never shortens one; buying a gifted Tool later grants over it (the licence becomes `generated`). Only a gift can be for good - a sale's licence edited to no expiry is refused. The Licences ledger has a Gifts filter; the licence expiry check runs hourly.
|
|
1430
|
+
- Where: domma-cms-manager `plugins/site-manager/services/entitlements.js`, `licenceManager.js`; `dcms-marketplace/scripts/build-catalogue.js` (`giftable`) · Since: Manager 2026-10-23 (Games)
|
|
1424
1431
|
- **Only our paid plugins are ever checked** - The gate keys on `closedSource: true`; a plugin you wrote yourself is never asked for an entitlement.
|
|
1425
1432
|
- **Where entitlements live** - `plugins/<slug>/.entitlement.json` (arrives in the envelope, survives upgrades), or instance-wide in `config/entitlements/*.json` (e.g. an owner wildcard `slug: "*"` covering every plugin, used by the manager and cms.dommajs.org). Per-plugin wins.
|
|
1426
1433
|
- **Offline, no phone-home** - Verification is Ed25519 over canonical JSON (keys sorted, no whitespace) against keys compiled into the CMS release.
|
|
@@ -1517,7 +1524,7 @@ plugins/my-plugin/
|
|
|
1517
1524
|
| `admin.routes[]`, `admin.views{}` | SPA routes and view modules `{entry, exportName}`. |
|
|
1518
1525
|
| `admin.css[]` | Admin stylesheets (may include `../_lib/admin/ui/help.css`). |
|
|
1519
1526
|
| `inject.head`, `inject.headLate`, `inject.bodyEnd` | HTML snippets added to every public page (paths confined to the plugin folder). |
|
|
1520
|
-
| `permissions[]` | Permission resources `{key, label, actions?, group?, icon?, grant?: ["admin"]}` - `grant` given once. |
|
|
1527
|
+
| `permissions[]` | Permission resources `{key, label, actions?, group?, icon?, grant?: ["admin"], collectionAction?}` - `grant` given once; `collectionAction` is the action the generic Collections API needs on the plugin's collections (default: every action). |
|
|
1521
1528
|
| `settingsSchema[]` | Configure form: `{key, label, type: text|number|select|boolean, options?, min?, max?, help?, secret?}`. |
|
|
1522
1529
|
| `requires[]` / `uses{}` | Hard / soft dependencies on Tools or plugins (0.88.0). |
|
|
1523
1530
|
| `supersedes[]` | Plugins or built-in Tools this one replaces. |
|
|
@@ -2410,16 +2417,47 @@ Tier: paid (closed source) · Version: 0.4.0 · Requires: nothing; no MongoDB (w
|
|
|
2410
2417
|
|
|
2411
2418
|
**Where:** Admin sidebar "Sheets" (`#/plugins/sheets`).
|
|
2412
2419
|
|
|
2413
|
-
### Quiz (`quiz`)
|
|
2420
|
+
### Games: Quiz (`quiz`), Crossword (`crossword`), Word Search (`word-search`) and Daily Word (`daily-word`)
|
|
2414
2421
|
|
|
2415
|
-
Tier:
|
|
2422
|
+
Tier: paid and giftable (the Manager can gift them for good or for a number of days) · Versions: Quiz 1.4.0, Crossword 1.4.0, Word Search 1.1.0, Daily Word 1.2.0 (need Domma CMS 1.9.0 for the shared games library and its motion files) · Sidebar: Tools > Games · Permissions: a Games group in the role editor
|
|
2423
|
+
|
|
2424
|
+
All four share the engine's games library (`plugins/_lib/games`: plays, tokens, names, ranking, timing; `plugins/_lib/public/games`: player parts and motion; `plugins/_lib/admin/games/kit.js`: list, editor, test and plays screens). The server keeps the clock and marks every move; nothing that gives an answer reaches the browser before the play is finished. Name-only players, honeypot and rate limits, test plays from the Tool, a sidebar badge, notifications. The generic Collections API reaches a game's collections only with the game's `manage` action (`collectionAction` in plugin.json).
|
|
2425
|
+
|
|
2426
|
+
#### Quiz and Crossword
|
|
2416
2427
|
|
|
2417
2428
|
**1.0.0 (CMS 1.8.32)** - see the 1.8.32 changelog: server-kept time and marking, name-only players with a play token, leaderboards by period (best go per name), Plays screen (hide, delete, reset), sidebar badge, top-spot notifications, `[quiz]`/`[crossword]` and `-leaderboard` shortcodes.
|
|
2418
2429
|
|
|
2419
2430
|
**1.1.0 - Test from the Tool, domma-reactive admin.** **Test** (editor, list rows, Plays screen; `#/plugins/quiz/test/<id>`, `#/plugins/crossword/test/<id>`) mounts the real public player in the admin and plays the SAVED game whatever its status or dates ("Save and test" when there are unsaved changes). The play is stored with `test: true` and played through the ordinary `/public/play` routes, but `PlayStore` leaves it out of boards, places, goes-per-name, the admin's plays list and counts and the sidebar badge; it never notifies; the next test of the game clears it. The result says where it would have come (`wouldPlace`). Routes: `GET/POST /api/plugins/quiz/quizzes/:id/test`, `GET/POST /api/plugins/crossword/puzzles/:id/test` (manage permission). Both admin screens are rebuilt on domma-reactive through the shared kit (`plugins/_lib/admin/games/kit.js`: `mountScreen`, `wireRows`, `sortableRows`, `saveKey`, `playsScreen`, `testScreen`): list rows with a context menu (right-click, ⋮, Shift+F10), questions dragged by a grip to reorder, Ctrl+S, help popovers; the crossword preview grid uses fixed ink and paper colours.
|
|
2420
2431
|
|
|
2432
|
+
**1.2.0 - export and import.** `GET /export` (all) and `GET /quizzes/:id/export` / `GET /puzzles/:id/export` (one) give a file `{format: 'domma-quiz'|'domma-crossword', version: 1, exportedAt, quizzes|puzzles}` - games without ids, timestamps or crossword layouts, never plays. `POST /import` (manage, 8 MB, 50 a file) cleans each item as on save and adds it as a draft with a unique short name; invalid items are skipped with their reason; a file of the other game or a newer version is refused by name. Crossword grids are re-laid from words + seed, so they match. Admin: Import… and Export all on the list, Export in each row's menu and the editor's banner.
|
|
2433
|
+
|
|
2434
|
+
**1.3.0 - paid and giftable.** `tier: paid`, `closedSource`, `giftable`: they reach a site through the catalogue and a licence, or a gift from the Manager (for good or for a number of days), no longer inside the engine (`plugins/free-tier.lock.json` no longer lists them). The engine update removes a site's old free copy (`bin/lib/left-free-tier.js`): never a copy with `.entitlement.json` beside it, never one whose plugin.json says paid, never `data/` or `backups/`. Their permissions sit in a Games group in the role editor. Their tests live with them in dcms-marketplace; the shared `_lib/games` tests stay in `tests/plugins/games-pure.test.js`.
|
|
2435
|
+
|
|
2436
|
+
**1.4.0 - hints, boards, personal best, share, animations.** See "Shared by all four" below.
|
|
2437
|
+
|
|
2421
2438
|
**Where:** Tools > Games > Quiz (`#/plugins/quiz`) and Crossword (`#/plugins/crossword`).
|
|
2422
2439
|
|
|
2440
|
+
#### Word Search (`word-search`)
|
|
2441
|
+
|
|
2442
|
+
**1.0.0** - The admin types up to 40 words (3-15 letters; spaces and hyphens dropped, accents folded) and an optional theme; the server lays out the grid (`public/lib/layout.js`, pure and seeded: 8x8 to 20x20 or Auto; across, down, diagonal, each a switch, plus "backwards too"; Shuffle for another layout; words that would not fit come back as `unplaced`). The fill never makes a second copy of a listed word or a blocked word (`BLOCKED_WORDS` from `_lib/games/names.js`, exported for this) in any direction. Players mark a word by its first and last letter - drag, tap-tap or the keyboard; the server reads the letters along the line and accepts an unfound word either way round; a wrong mark costs seconds (default 5); the last word finishes the play, fastest full solve wins. The word list can be shown or hidden. Word positions never reach the browser before the play finishes. Changing the words of an open word search ends plays in progress. Shortcodes `[word-search id="..." /]`, `[word-search-leaderboard id="..." /]`; notification source `fastest`. **1.1.0** - the extras below.
|
|
2443
|
+
|
|
2444
|
+
**Where:** Tools > Games > Word Search (`#/plugins/word-search`).
|
|
2445
|
+
|
|
2446
|
+
#### Daily Word (`daily-word`)
|
|
2447
|
+
|
|
2448
|
+
**1.0.0** - One hidden word a day, the same for everyone. A site makes word sets (length 4-8, guesses 4-8, default 6, a start date, answers in a seeded order fixed at save, Shuffle order); day N uses answer N; a Calendar view shows what is coming (manage permission only); a set running out warns a week ahead (notification `running-low`) and the public side shows "No word today" (`no-word`). Guesses must be real words (a bundled public-domain English list, server side only, switchable per set to "any letters") or one of the set's answers, else "Not in the word list" and no guess is used. Marking follows the duplicate-letter rule (`mark()`, pure). The site's day decides the word; one go per name per day by default. An **Archive** of past days is playable but never on a board, in a streak or counted for goes. Score = guesses allowed + 1 - guesses used, then the fastest time. Share copies the coloured squares only. Changing today's or a past day's answer is refused once played. Export holds future answers, so it needs manage. **1.1.0 - streaks**: the finished screen shows the name's streak (days in a row with a solved real play) or that a spent day ended it; `order="streak"` on `[daily-word-leaderboard]` ranks current streaks; the goes check and the new play are one turn per set, day and name (no double go in a race). **1.2.0** - the extras below. Shortcodes `[daily-word id="..." /]`, `[daily-word-leaderboard id="..." period="today" /]`.
|
|
2449
|
+
|
|
2450
|
+
**Where:** Tools > Games > Daily Word (`#/plugins/daily-word`).
|
|
2451
|
+
|
|
2452
|
+
#### Shared by all four
|
|
2453
|
+
|
|
2454
|
+
- **Giftable** - paid Tools that reach a site through the catalogue and a licence, or a gift from the Manager for good or for a number of days ([Tiers, licences and entitlements](#tiers-licences-and-entitlements)). Quiz and Crossword left the free tier at 1.3.0; the engine update (1.9.0) removes a site's old free copy unless it is licensed, keeping `data/` and `backups/`.
|
|
2455
|
+
- **Export and import** - every game exports one or all as a file and imports a file as drafts beside what is there, with no plays; layouts and schedules are worked out afresh on the server.
|
|
2456
|
+
- **Hints** - per game: on or off, hints a play, seconds each. Quiz takes two wrong answers away or gives a typed answer's first letter; Crossword fills one letter of the chosen word; Word Search shows where one unfound word starts; Daily Word shows one letter in its place. The server decides and records each (`POST /public/play/:id/hint`); results, the Plays screen and boards (an "H2" chip) show hints used.
|
|
2457
|
+
- **Boards** - `board="show|hide|only"` on every game shortcode; a per-game **Leaderboard shows** period (`boardPeriod`) used when no period is asked for; `[games-leaderboards games="..." period="..." limit="..."]` puts every Game's boards on one page (each game registers the same handler, so it works with any of them installed). Board shortcodes on the page refresh when a game finishes.
|
|
2458
|
+
- **Personal best** on every finished screen; **Share** (no answers) on Quiz, Crossword and Word Search, coloured squares on Daily Word.
|
|
2459
|
+
- **Animations** - a per-game setting, on unless switched off: answers, letters and tiles pop or flip in, right and wrong answers pulse, found words pop along their line, finished screens and board rows pop in. Never under `prefers-reduced-motion` or `html.dm-reduced-motion`. Shared CSS and helpers: `plugins/_lib/public/games/motion.css` and `motion.js` (engine 1.9.0).
|
|
2460
|
+
|
|
2423
2461
|
### Other plugin directories
|
|
2424
2462
|
|
|
2425
2463
|
No other directories exist in `dcms-marketplace/plugins/` beyond the plugins above and `_lib`. `_lib` is not a plugin (skipped by plugin discovery): it holds the shared mail core used by Mail Reader and Email Pro, plus shared `dataStore.js` and `schemaSync.js`.
|
|
@@ -2516,6 +2554,8 @@ Later changes to these features are in the CHANGELOG (for example 0.92.1: row-le
|
|
|
2516
2554
|
|
|
2517
2555
|
### Plugins & the Marketplace platform
|
|
2518
2556
|
|
|
2557
|
+
- A games page: a Daily Word set with a month of answers, a Word Search and a Quiz on one page, with `[games-leaderboards]` below them; hints with a penalty, and moving a game to another site with Export and Import.
|
|
2558
|
+
- Gifting a Tool from the Manager: gift a Game for 30 days, extend it, make it for good, and end it.
|
|
2519
2559
|
- Build your first plugin in 10 minutes: New plugin in the Marketplace, add a route and a screen in the code editor, restart, see it in the sidebar.
|
|
2520
2560
|
- Add a custom shortcode and let its HTML through the sanitiser (`registerShortcode` + `registerSanitizeRules`).
|
|
2521
2561
|
- Give your plugin a Configure form with `settingsSchema`, including a masked secret read at request time.
|
|
@@ -318,8 +318,12 @@ Columns animate into view one by one as the row scrolls into the viewport:
|
|
|
318
318
|
| Attribute | Type | Description |
|
|
319
319
|
|---|---|---|
|
|
320
320
|
| `span` | number | Number of columns to span (`col-span-N`) |
|
|
321
|
+
| `align` | string | Content alignment: `left`, `center` / `centre`, or `right`. Inherits alignment when omitted. |
|
|
322
|
+
| `left`, `center`, `centre`, `right` | flag | Shorthand alignment, e.g. `[col centre]`. An explicit `align` takes precedence. |
|
|
321
323
|
| `id` | string | HTML `id` attribute |
|
|
322
324
|
|
|
325
|
+
`[col align="centre"]Content[/col]` centres the column content. You can also use `[col center]`, `[col centre]`, `[col left]`, or `[col right]`.
|
|
326
|
+
|
|
323
327
|
`[col]` also accepts injected effects (`reveal`, `breathe`, `animate`, `ripple`) - see [Effect injection](#effect-injection).
|
|
324
328
|
|
|
325
329
|
### Rendered output
|
|
@@ -211,6 +211,13 @@ each role sees.
|
|
|
211
211
|
- `group` (default `Plugins`) and `icon` place it in the role editor.
|
|
212
212
|
- `grant` gives those existing roles the permission **once**. The grant is recorded on the role, so if the admin later
|
|
213
213
|
removes it, it stays removed. The super-admin (level 0) holds every plugin permission without a grant.
|
|
214
|
+
- `collectionAction` names the action someone needs to reach your plugin's collections through core's generic
|
|
215
|
+
routes - the Collections API and screen, a form's submissions, Views and API Builder endpoints - `"manage"`, say.
|
|
216
|
+
It must be one of the resource's `actions` or it is ignored. Without it, every action of the resource is needed.
|
|
217
|
+
Holding the gate of ANY one of your resources opens all your collections, so if a resource's only action is
|
|
218
|
+
`read`, give the others a `collectionAction` and keep row secrets (answers, solutions, tokens) in a plugin whose
|
|
219
|
+
every resource is gated. A plugin with no permissions at all leaves its collections to `collections` holders.
|
|
220
|
+
Switched off, your collections keep the gate your `plugin.json` and role files declare.
|
|
214
221
|
|
|
215
222
|
Guard your routes with them:
|
|
216
223
|
|
package/package.json
CHANGED
|
@@ -16,6 +16,12 @@ export const NAME_MAX = 24;
|
|
|
16
16
|
/** Folded to a-z first, so leetspeak and accents do not slip past. Kept short on purpose. */
|
|
17
17
|
const BLOCKED = ['fuck', 'shit', 'cunt', 'wank', 'twat', 'bitch', 'bastard', 'nigg', 'fag', 'slut', 'whore', 'rape', 'nazi', 'hitler', 'penis', 'vagina', 'dick', 'cock', 'pussy', 'arse', 'asshole', 'admin', 'moderator'];
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The same list, for games that put letters in front of the public (Word Search fills its grid with
|
|
21
|
+
* random letters and rerolls any fill that spells one of these). Read-only.
|
|
22
|
+
*/
|
|
23
|
+
export const BLOCKED_WORDS = Object.freeze([...BLOCKED]);
|
|
24
|
+
|
|
19
25
|
const LEET = {0: 'o', 1: 'i', 3: 'e', 4: 'a', 5: 's', 7: 't', '@': 'a', $: 's'};
|
|
20
26
|
|
|
21
27
|
/** Lower case, accents gone, spacing collapsed - the identity of a name. */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.gm-motion{--gm-step: 60ms;--gm-dur: .4s}.gm-motion :is(.gm-pop,.gm-pop-list>*,.gm-flip,.gm-bounce){animation-duration:var(--gm-dur);animation-timing-function:cubic-bezier(.22,.61,.36,1);animation-fill-mode:backwards;animation-delay:calc(var(--gm-i, 0) * var(--gm-step))}.gm-motion :is(.gm-pop,.gm-pop-list>*){animation-name:dm-game-pop}.gm-motion .gm-flip{animation-name:dm-game-flip}.gm-motion .gm-bounce{animation-name:dm-game-bounce;animation-fill-mode:none}.gm-motion .gm-shake{animation:dm-game-shake .45s ease}.gm-motion .gm-pulse-good{animation:dm-game-pulse-good .5s ease}.gm-motion .gm-pulse-bad{animation:dm-game-pulse-bad .5s ease}.gm-motion .gm-hint-pop{animation:dm-game-hint .5s cubic-bezier(.22,.61,.36,1)}.gm-motion .gm-pop-list>:nth-child(2){--gm-i: 1}.gm-motion .gm-pop-list>:nth-child(3){--gm-i: 2}.gm-motion .gm-pop-list>:nth-child(4){--gm-i: 3}.gm-motion .gm-pop-list>:nth-child(5){--gm-i: 4}.gm-motion .gm-pop-list>:nth-child(6){--gm-i: 5}.gm-motion .gm-pop-list>:nth-child(7){--gm-i: 6}.gm-motion .gm-pop-list>:nth-child(8){--gm-i: 7}.gm-motion .gm-pop-list>:nth-child(9){--gm-i: 8}.gm-motion .gm-pop-list>:nth-child(10){--gm-i: 9}.gm-motion .gm-pop-list>:nth-child(11){--gm-i: 10}.gm-motion .gm-pop-list>:nth-child(12){--gm-i: 11}.gm-motion .gm-pop-list>:nth-child(13){--gm-i: 12}.gm-motion .gm-pop-list>:nth-child(14){--gm-i: 13}.gm-motion .gm-pop-list>:nth-child(15){--gm-i: 14}.gm-motion .gm-pop-list>:nth-child(16){--gm-i: 15}.gm-motion .gm-pop-list>:nth-child(17){--gm-i: 16}.gm-motion .gm-pop-list>:nth-child(18){--gm-i: 17}.gm-motion .gm-pop-list>:nth-child(19){--gm-i: 18}.gm-motion .gm-pop-list>:nth-child(20){--gm-i: 19}.gm-motion .gm-pop-list>:nth-child(n+21){--gm-i: 20}@keyframes dm-game-pop{0%{opacity:0;transform:scale(.5)}70%{opacity:1;transform:scale(1.08)}to{opacity:1;transform:scale(1)}}@keyframes dm-game-flip{0%{transform:perspective(300px) rotateX(-90deg)}to{transform:perspective(300px) rotateX(0)}}@keyframes dm-game-bounce{0%,to{transform:translateY(0)}40%{transform:translateY(-22%)}70%{transform:translateY(4%)}}@keyframes dm-game-shake{0%,to{translate:0}20%,60%{translate:-6px}40%,80%{translate:6px}}@keyframes dm-game-pulse-good{0%{transform:scale(1)}40%{transform:scale(1.06);box-shadow:0 0 0 6px color-mix(in srgb,#2f7d32 35%,transparent)}to{transform:scale(1)}}@keyframes dm-game-pulse-bad{0%{transform:scale(1)}40%{transform:scale(.97);box-shadow:0 0 0 6px color-mix(in srgb,#b3261e 35%,transparent)}to{transform:scale(1)}}@keyframes dm-game-hint{0%{transform:scale(.6)}60%{transform:scale(1.2)}to{transform:scale(1)}}@media(prefers-reduced-motion:reduce){.gm-motion,.gm-motion *{animation:none!important}}html.dm-reduced-motion .gm-motion,html.dm-reduced-motion .gm-motion *{animation:none!important}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Games - motion helpers for the public players (the stylesheet is motion.css
|
|
3
|
+
* beside this file; see its header for the classes).
|
|
4
|
+
*
|
|
5
|
+
* MOTION_CSS the stylesheet, for addStyles([...])
|
|
6
|
+
* motionOff() true when the visitor asked for less motion (OS setting or the footer switch)
|
|
7
|
+
* replay(el, cls) run a one-shot animation class again on an element already drawn
|
|
8
|
+
*
|
|
9
|
+
* Decoration only: none of these waits for an animation before anything else happens.
|
|
10
|
+
*
|
|
11
|
+
* @module _lib/public/games/motion
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const MOTION_CSS = '/plugins/_lib/public/games/motion.css';
|
|
15
|
+
|
|
16
|
+
export function motionOff() {
|
|
17
|
+
if (document.documentElement.classList.contains('dm-reduced-motion')) return true;
|
|
18
|
+
try { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { return false; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Run `cls` again on `el` (removed, a reflow, added back); cleared once it ends. */
|
|
22
|
+
export function replay(el, cls) {
|
|
23
|
+
if (!el || motionOff()) return;
|
|
24
|
+
el.classList.remove(cls);
|
|
25
|
+
void el.offsetWidth;
|
|
26
|
+
el.classList.add(cls);
|
|
27
|
+
el.addEventListener('animationend', () => el.classList.remove(cls), {once: true});
|
|
28
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"note": "Written by `make sync-free-tier` in dcms-marketplace. Do not edit these plugins here - edit them there and sync.",
|
|
3
|
-
"source": "dcms-marketplace@
|
|
3
|
+
"source": "dcms-marketplace@4201b7a",
|
|
4
4
|
"plugins": {
|
|
5
5
|
"blog": {
|
|
6
6
|
"version": "1.10.1",
|
|
@@ -126,27 +126,6 @@
|
|
|
126
126
|
"tests/seo.test.js": "8d935319eb21fbb81feb663220384b3a4d52d0fe8597047994652daa8c981489"
|
|
127
127
|
}
|
|
128
128
|
},
|
|
129
|
-
"crossword": {
|
|
130
|
-
"version": "1.1.0",
|
|
131
|
-
"files": {
|
|
132
|
-
"CLAUDE.md": "598736eec2dfa833f7bb84ca9376071d1143d98a5c01106d0ff453749af18e43",
|
|
133
|
-
"admin/css/index.css": "d8d32268504d5aefa9c5fbf48a0c67b978cdd01d3be0bc747205a001972e05db",
|
|
134
|
-
"admin/templates/crossword.html": "336b4a3b7efff1ab8004c0516d55f11ae2efdc98cb3b18133ea00ca07311e2aa",
|
|
135
|
-
"admin/views/crossword.js": "42f3aabd09483eddf34d25dae243a853c562a13348eafe4fc1977361c1b03f4e",
|
|
136
|
-
"collections/crossword-plays.json": "20577250f00ebe0f6503cc2aae83dba93d88381b76b0b1dbc76c7b856d4295ff",
|
|
137
|
-
"collections/crossword-puzzles.json": "1b70950c96e50ede4d6a0c8e59ca7767b3c684c1a7c68588dc6462e69dad596e",
|
|
138
|
-
"config.js": "7a15db33506a432f5c43a095366c2e82a3bd20ceded585fb4fb78b93ad6dff29",
|
|
139
|
-
"docs/guide.md": "1189300bbe20f032e9040fe027e84a6137ab560e2fe9b88219aba66ea336ec4d",
|
|
140
|
-
"inject/body.html": "9dad7927d5afbd5e53f1edaa4a420403cd2325ce908624f9643a087388c090ab",
|
|
141
|
-
"lib/crossword.js": "f8149db65fb3a7f82b2fbae6a9f8d3ceeeec95730e426cf6fc36ea058527df34",
|
|
142
|
-
"lib/store.js": "284b3b8abe9fdaa64a660d5ab0120856b893d166b28e8bda6cf0ef038baa74bc",
|
|
143
|
-
"plugin.js": "5c1885955d122200c7ff9629d7fb9fcb54c232edb4cec4658574ca90d73730e5",
|
|
144
|
-
"plugin.json": "c5f9e3d934423161af6ef5fbf52c0c3394e737245a4de21da72da6d75a790f44",
|
|
145
|
-
"public/crossword.css": "656f369ea6587b000ff56b8ab80010f4f76af10966537c180950074ec5169f61",
|
|
146
|
-
"public/crossword.js": "8418a2c52a3d351a95e271d3c9f564b7336243ccde75106a9998995575802173",
|
|
147
|
-
"public/lib/layout.js": "0026afd8b42277838c55e1c031fa07cfea1c640c47aaf385e461cc998c4619c0"
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
129
|
"docs": {
|
|
151
130
|
"version": "2.14.0",
|
|
152
131
|
"files": {
|
|
@@ -270,27 +249,6 @@
|
|
|
270
249
|
"tests/messages.test.js": "5b724e53fcf7f3b5a035c47c0a4ca98dcd7de42b65f4a4ea195a9409fd53e8af"
|
|
271
250
|
}
|
|
272
251
|
},
|
|
273
|
-
"quiz": {
|
|
274
|
-
"version": "1.1.0",
|
|
275
|
-
"files": {
|
|
276
|
-
"CLAUDE.md": "9d037c11b0de115bf17a6c53387116be037c0d4dcd1b6bf3dafa373539143e16",
|
|
277
|
-
"admin/css/index.css": "6598225cfb8b58c486eb390ac41c7f359c78f48e0c97bc8b354d3832f618afa8",
|
|
278
|
-
"admin/templates/quiz.html": "f0b65c406b0b6325a815906ca8da7d884d5d24f2504e05631e46c34a5ca0edb9",
|
|
279
|
-
"admin/views/quiz.js": "c58ba86ec989df8f29e8acff2266fb3463e301c5516bf9281193de539b7c1f44",
|
|
280
|
-
"collections/quiz-plays.json": "331d665a90b6dddf069245a2faf8dd454a36d387702343a7e36d1fe6dd5015f8",
|
|
281
|
-
"collections/quiz-quizzes.json": "ce9bacf6c05fb5c22f9a5c5105f624c7a9c6d78c2bd343072d9cb7587e920b1d",
|
|
282
|
-
"config.js": "24db3727a804678ef12290ae258c7cf52afbc7a425bb452d87b7b2467ff7c559",
|
|
283
|
-
"docs/guide.md": "a7f8f449b94ecc726dc82fc7a0ae7c335431fa757f9493859bdf16e633689c3f",
|
|
284
|
-
"inject/body.html": "07727ead4a04fdeb2759a19e8cc358ba2692b807fb741a6daafaf02463fe8233",
|
|
285
|
-
"lib/engine.js": "a554c7abfa357288bbd73f47c9bcbf8eab7d8222ff63b1fd119a3ca5f9fb79f4",
|
|
286
|
-
"lib/quiz.js": "d697bbd1af41575a9ecee49781695f2dfb4a585bf0230cc39ac5d174dc9c2762",
|
|
287
|
-
"lib/store.js": "a740d879185e20c036ac17ab5cbca74d1f71097f4af3bc121fcddc826d8686c6",
|
|
288
|
-
"plugin.js": "0ec6f96fc4922c7ef41b36bd757c02926ee99d50e78fe85f7f30b9fbd9605b25",
|
|
289
|
-
"plugin.json": "788960b6a31c7c1c2d525fb7cae9ab9b7c9310def5d778c275f22302ecaadf78",
|
|
290
|
-
"public/quiz.css": "e8869fb30e22763357464e9aa7a353306b6631b8a7418ba4e483331e66aada67",
|
|
291
|
-
"public/quiz.js": "dedc96469e440c979b20d7d95655fbb25062b00b27be569b2a55ec5a8ec0cbb5"
|
|
292
|
-
}
|
|
293
|
-
},
|
|
294
252
|
"security": {
|
|
295
253
|
"version": "1.2.1",
|
|
296
254
|
"files": {
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from '../../services/apiEndpoints.js';
|
|
26
26
|
import {canSeeArtefact} from '../../services/projects.js';
|
|
27
27
|
import {getCollection} from '../../services/collections.js';
|
|
28
|
+
import {pluginDenial} from '../../services/collectionAccess.js';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Scoped users must not expose data they cannot see: the collection an
|
|
@@ -40,7 +41,10 @@ async function canUseCollection(user, slug) {
|
|
|
40
41
|
if (!slug) return true;
|
|
41
42
|
const schema = await getCollection(slug).catch(() => null);
|
|
42
43
|
if (!schema) return true;
|
|
43
|
-
|
|
44
|
+
// A plugin's collection needs what the Collections API asks for it - an
|
|
45
|
+
// endpoint would otherwise publish rows (a game's answers) its author
|
|
46
|
+
// could not read.
|
|
47
|
+
return canSeeArtefact(user, schema) && !pluginDenial(schema, user);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
@@ -81,9 +81,10 @@ const ALL_PRESET_SLUGS = new Set(['roles', 'user-profiles', ...PRESET_COLLECTION
|
|
|
81
81
|
/**
|
|
82
82
|
* The generic routes are open to anyone holding `collections`, which every
|
|
83
83
|
* level-1 admin does. Two kinds of collection need more than that (see
|
|
84
|
-
* services/collectionAccess.js): a plugin's collection needs
|
|
85
|
-
*
|
|
86
|
-
*
|
|
84
|
+
* services/collectionAccess.js): a plugin's collection needs full use of one of
|
|
85
|
+
* the plugin's own permissions (its declared `collectionAction`, or every
|
|
86
|
+
* action), and the Tools' user-data presets (notes, tasks, contacts) are kept
|
|
87
|
+
* to the caller's own rows. Answers the schema, or sends 404/403 and
|
|
87
88
|
* answers null - the caller returns at once.
|
|
88
89
|
*
|
|
89
90
|
* @param {object} request
|