domma-cms 1.8.32 → 1.8.34
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 +9 -0
- package/docs/DommaFeatures.md +13 -1
- package/package.json +1 -1
- package/plugins/_lib/admin/games/games.css +1 -1
- package/plugins/_lib/admin/games/kit.js +329 -124
- package/plugins/_lib/games/http.js +11 -1
- package/plugins/_lib/games/plays.js +28 -7
- package/plugins/crossword/CLAUDE.md +37 -0
- package/plugins/crossword/admin/css/index.css +18 -1
- package/plugins/crossword/admin/views/crossword.js +337 -205
- package/plugins/crossword/docs/guide.md +53 -0
- package/plugins/crossword/inject/body.html +1 -1
- package/plugins/crossword/plugin.js +38 -5
- package/plugins/crossword/plugin.json +4 -3
- package/plugins/crossword/public/crossword.css +36 -1
- package/plugins/crossword/public/crossword.js +468 -6
- package/plugins/docs/CLAUDE.md +318 -0
- package/plugins/docs/admin/css/docs.css +783 -0
- package/plugins/docs/admin/lib/api.js +340 -0
- package/plugins/docs/admin/lib/convert.js +482 -0
- package/plugins/docs/admin/lib/drafts.js +115 -0
- package/plugins/docs/admin/lib/editor.js +851 -0
- package/plugins/docs/admin/lib/find-replace.js +426 -0
- package/plugins/docs/admin/lib/folders.js +203 -0
- package/plugins/docs/admin/lib/panel-guard.js +73 -0
- package/plugins/docs/admin/lib/pdf-view.js +126 -0
- package/plugins/docs/admin/lib/send.js +156 -0
- package/plugins/docs/admin/lib/settings.js +142 -0
- package/plugins/docs/admin/lib/share.js +323 -0
- package/plugins/docs/admin/lib/shortcuts.js +44 -0
- package/plugins/docs/admin/lib/templates.js +194 -0
- package/plugins/docs/admin/lib/versions.js +192 -0
- package/plugins/docs/admin/templates/docs.html +92 -0
- package/plugins/docs/admin/templates/editor.html +86 -0
- package/plugins/docs/admin/views/docs.js +777 -0
- package/plugins/docs/admin/views/editor.js +978 -0
- package/plugins/docs/config.js +24 -0
- package/plugins/docs/docs/guide.md +270 -0
- package/plugins/docs/lib/extensions.js +213 -0
- package/plugins/docs/lib/importer.js +72 -0
- package/plugins/docs/lib/pdf-worker.js +48 -0
- package/plugins/docs/lib/pdf.js +222 -0
- package/plugins/docs/lib/store.js +748 -0
- package/plugins/docs/plugin.js +1108 -0
- package/plugins/docs/plugin.json +89 -0
- package/plugins/docs/plugin.public.js +114 -0
- package/plugins/docs/templates/public-doc.html +36 -0
- package/plugins/docs/tests/_env.js +10 -0
- package/plugins/docs/tests/_pdf.js +73 -0
- package/plugins/docs/tests/convert.test.js +95 -0
- package/plugins/docs/tests/drafts.test.js +82 -0
- package/plugins/docs/tests/pdf.test.js +86 -0
- package/plugins/docs/tests/plugin.test.js +816 -0
- package/plugins/docs/tests/shortcuts.test.js +30 -0
- package/plugins/free-tier.lock.json +147 -1
- package/plugins/garage/CLAUDE.md +51 -0
- package/plugins/garage/admin/css/garage.css +73 -0
- package/plugins/garage/admin/lib/garage.js +236 -0
- package/plugins/garage/admin/templates/garage.html +86 -0
- package/plugins/garage/admin/views/garage.js +581 -0
- package/plugins/garage/collections/garage-vehicles/schema.json +101 -0
- package/plugins/garage/config.js +18 -0
- package/plugins/garage/docs/guide.md +118 -0
- package/plugins/garage/plugin.js +471 -0
- package/plugins/garage/plugin.json +68 -0
- package/plugins/garage/tests/access.test.js +134 -0
- package/plugins/garage/tests/garage.test.js +106 -0
- package/plugins/messages/CLAUDE.md +230 -0
- package/plugins/messages/admin/css/messages.css +117 -0
- package/plugins/messages/admin/lib/messages.js +452 -0
- package/plugins/messages/admin/templates/messages.html +76 -0
- package/plugins/messages/admin/views/messages.js +1219 -0
- package/plugins/messages/collections/user-message-threads/schema.json +22 -0
- package/plugins/messages/collections/user-messages/schema.json +23 -0
- package/plugins/messages/config.js +14 -0
- package/plugins/messages/docs/guide.md +176 -0
- package/plugins/messages/lib/call.js +138 -0
- package/plugins/messages/plugin.js +994 -0
- package/plugins/messages/plugin.json +59 -0
- package/plugins/messages/plugin.public.js +52 -0
- package/plugins/messages/templates/call.html +6 -0
- package/plugins/messages/tests/after-send.test.js +22 -0
- package/plugins/messages/tests/api.test.js +187 -0
- package/plugins/messages/tests/call.test.js +497 -0
- package/plugins/messages/tests/drafts.test.js +25 -0
- package/plugins/messages/tests/messages.test.js +167 -0
- package/plugins/quiz/CLAUDE.md +43 -0
- package/plugins/quiz/admin/css/index.css +13 -1
- package/plugins/quiz/admin/views/quiz.js +423 -245
- package/plugins/quiz/docs/guide.md +61 -0
- package/plugins/quiz/inject/body.html +1 -1
- package/plugins/quiz/plugin.js +39 -5
- package/plugins/quiz/plugin.json +4 -3
- package/plugins/quiz/public/quiz.css +36 -1
- package/plugins/quiz/public/quiz.js +298 -6
- package/plugins/theme-roller/CLAUDE.md +65 -0
- package/plugins/theme-roller/admin/css/theme-roller.css +43 -0
- package/plugins/theme-roller/admin/lib/prompt.js +22 -0
- package/plugins/theme-roller/admin/lib/themes.js +196 -0
- package/plugins/theme-roller/admin/lib/view.js +88 -0
- package/plugins/theme-roller/admin/templates/theme-roller.html +73 -0
- package/plugins/theme-roller/admin/views/theme-roller-view.js +639 -0
- package/plugins/theme-roller/config.js +1 -0
- package/plugins/theme-roller/docs/guide.md +104 -0
- package/plugins/theme-roller/plugin.js +263 -0
- package/plugins/theme-roller/plugin.json +42 -0
- package/plugins/theme-roller/tests/api.test.js +161 -0
- package/plugins/theme-roller/tests/prompt-values.test.js +22 -0
- package/plugins/theme-roller/tests/themes.test.js +77 -0
- package/plugins/theme-roller/tests/view.test.js +55 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# Docs - AI Assistant Guide
|
|
2
|
+
|
|
3
|
+
A document editor in the admin (2.0): Home and a per-document editor, sharing with people, public
|
|
4
|
+
links, import/export, send to page/blog/email, an edit lock, trash, stars, version history, templates.
|
|
5
|
+
Free (2.14.0; paid and closed source until then). Plan and phases (with Docs Pro): `dcms-marketplace/docs/docs-plan.md`.
|
|
6
|
+
|
|
7
|
+
## Files
|
|
8
|
+
|
|
9
|
+
| File | What |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `plugin.js` | the admin API (`/api/plugins/docs`) |
|
|
12
|
+
| `plugin.public.js` | the public link page `GET /docs/s/:token` (`?download=md|html|txt`), via `renderBlogPage` + `templates/public-doc.html` |
|
|
13
|
+
| `lib/store.js` | storage, `accessOf`, user state (stars/recent), the edit lock table, link index, `sanitizeDoc`, `selectRows`, `purgeTrash`. Shared by both halves: its module state (`runtime.config`, locks, caches) is one copy. |
|
|
14
|
+
| `lib/importer.js` | `.md` (CMS `marked`, GFM task lists -> checklists), `.html`, `.txt` -> sanitised HTML |
|
|
15
|
+
| `lib/pdf.js`, `lib/pdf-worker.js` | 2.13: `inspectPdf` (the checks a PDF passes before it is kept), `extractPdfText` (pdfjs-dist in a worker thread with a time budget), `extractToHtml`, `contentDisposition` |
|
|
16
|
+
| `admin/lib/pdf-view.js` | 2.13: the PDF screen's parts - the viewer frame, facts, download, replace, the PDF download menu |
|
|
17
|
+
| `lib/extensions.js` | the registry `globalThis[Symbol.for('domma.docs')]` for Docs Pro |
|
|
18
|
+
| `admin/lib/convert.js` | pure, DOM-free: `parseHtml`, `htmlToMarkdown`, `htmlToText`, `countWords`, `preview`, `outline`, `standaloneHtml`, `startsWithTitle`. Used by server AND browser. |
|
|
19
|
+
| `admin/lib/shortcuts.js` | pure: Markdown typing shortcuts |
|
|
20
|
+
| `admin/lib/editor.js` | `DocEditor` - contenteditable + execCommand, toolbar, shortcuts as DOM work, checklists, tables, image paste/upload |
|
|
21
|
+
| `admin/lib/api.js` | `docsApi` (through `H`), `loadKit`, `loadExtensions`, `sanitizeHtml`, download/print helpers |
|
|
22
|
+
| `admin/lib/share.js`, `send.js`, `settings.js`, `versions.js`, `folders.js`, `templates.js`, `find-replace.js` | panels and helpers |
|
|
23
|
+
| `admin/views/docs.js` | Home (`#/plugins/docs`) |
|
|
24
|
+
| `admin/views/editor.js` | the editor (`#/plugins/docs/d/:id`) |
|
|
25
|
+
|
|
26
|
+
## Storage - files only
|
|
27
|
+
|
|
28
|
+
| Path | Holds |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `data/documents/<id>.json` | `id, title, content (HTML), format:'html', folderId, tags, userId (owner), shares:[{userId, role:'viewer'|'editor', addedAt, addedBy}], publicLink:{token, createdAt, createdBy}|null, trashedAt, trashedBy, wordCount, createdAt, updatedAt, updatedBy` |
|
|
31
|
+
| `data/versions/<id>/<n>.json` | `num, title, content, createdAt, savedBy, replacedBy` |
|
|
32
|
+
| `data/user-state.json` | `{[userId]: {starred:[id], recent:[{id, at}]}}` |
|
|
33
|
+
| `data/folders.json`, `data/templates.json` | through `_lib/dataStore` (per user) |
|
|
34
|
+
| `data/.backup-markdown/` | originals of documents converted from the old Markdown editor |
|
|
35
|
+
| `data/files/<id>.pdf` | 2.13: a PDF document's file; `data/versions/<id>/<n>.pdf` is the file an earlier version was |
|
|
36
|
+
|
|
37
|
+
Document writes are atomic (tmp + rename) and serialised per document. **Every route that changes an
|
|
38
|
+
existing document uses `mutateDoc(id, fn)`**, which re-reads inside the per-document write chain (a
|
|
39
|
+
share saved while an autosave lands must not write back a stale body). Single process only. There is
|
|
40
|
+
no MongoDB mode. Tests set `DOCS_DATA_DIR` (tests/_env.js, imported first) to a temp dir.
|
|
41
|
+
|
|
42
|
+
## Rules that matter
|
|
43
|
+
|
|
44
|
+
- **HTML is the only stored format.** Every body goes through `sanitizeDoc` (sanitize-html allowlist:
|
|
45
|
+
document tags, a few inline styles, `data-type` on ul, `data-checked` on li; http/https/mailto/tel
|
|
46
|
+
links, data: only for img) on create, update, import, restore and template save; `full()` and the
|
|
47
|
+
public page sanitise again on the way out. `editorMode` was removed in 2.0. `migrateLegacy` (onReady)
|
|
48
|
+
upgrades every document/template without `format` once, per document: no HTML tags = Markdown ->
|
|
49
|
+
`marked`, otherwise sanitised (1.x stored bodies raw). Versions are upgraded on restore. Backups in
|
|
50
|
+
`data/.backup-markdown/`.
|
|
51
|
+
- **The admin sanitises with DOMPurify directly** (`api.js sanitizeHtml`): `ALLOW_DATA_ATTR:false` plus
|
|
52
|
+
only `data-type`/`data-checked` (`allowDocAttrs` for add-ons). Domma.sanitize drops all `data-*`
|
|
53
|
+
(checklists became bullets); allowing all would let a body's `data-on-click`/`data-help` reach the
|
|
54
|
+
admin's bindings. The editor also sets the body only AFTER applyBindings/helpPopovers/icons.scan.
|
|
55
|
+
No DOMPurify -> text only.
|
|
56
|
+
- **Lost updates:** the editor sends `baseUpdatedAt`; a text save from a stale copy -> 409 `{conflict}`
|
|
57
|
+
and the editor keeps its text as a copy. The heartbeat compares the lock's `updatedAt` too (a lock
|
|
58
|
+
that lapsed while a laptop slept).
|
|
59
|
+
- **Versions:** one person's saves within 10 minutes of their last save add none (`shouldSnapshot`);
|
|
60
|
+
restores always snapshot.
|
|
61
|
+
- **Trash** belongs to the owner: sharees get 404, send/shares/link -> 409, Trash view and Empty Trash
|
|
62
|
+
are the user's own docs only (even an admin with `scope:'all'`).
|
|
63
|
+
- **Email:** `allowSend` site setting; 50 addresses per person per day (in memory) on top of 10 per send
|
|
64
|
+
and 10 sends / 10 min.
|
|
65
|
+
- **Known gap:** the public page renders through the site template, so site analytics / injected
|
|
66
|
+
scripts on that page can see the token in `location.href`.
|
|
67
|
+
- **Access** (`accessOf`): owner > shares (editor/viewer) > `scope:'all'` (everyone reads, admins are
|
|
68
|
+
owners). No access -> 404; a viewer asking to edit -> 403. Owner only: move, trash/restore/delete,
|
|
69
|
+
removing editors (an editor may add people, never remove an editor). Duplicates are always the
|
|
70
|
+
copier's own, unshared, without a link.
|
|
71
|
+
- **Edit lock** (in memory, per browser tab `session`, TTL 90s, heartbeat 30s): `POST /lock` answers
|
|
72
|
+
200 `{ok:false, lock, canTakeOver}` when held elsewhere (never 423 - the admin's `H` hides error
|
|
73
|
+
bodies). `PUT` of title/content from another session -> 423. `force` only for the owner or the same
|
|
74
|
+
user. The editor that loses the lock saves its unsaved content as "<title> (your unsaved changes)"
|
|
75
|
+
via `POST /duplicate {title, content}`.
|
|
76
|
+
- **Protected links fail closed (2.3):** `doc.linkGuarded` (set by Docs Pro through `setLinkGuard`) means the public page
|
|
77
|
+
answers 404 unless an extension with a `publicGate` is registered (`registry.hasGate()`) - a lapsed Docs Pro never
|
|
78
|
+
opens a password-protected or expired link.
|
|
79
|
+
- **Public links:** a random 18-byte token; `docByToken` returns null when the site setting
|
|
80
|
+
`publicLinks` is false or the document is trashed. Rate-limited 60/min, `noindex`, `no-store`,
|
|
81
|
+
`no-referrer`. Turning a link off and on makes a new token.
|
|
82
|
+
- **Send to page/blog** is done in the browser through the core `/api/pages` and Blog `/api/plugins/blog/posts`
|
|
83
|
+
with the user's own token (Markdown body, drafts) - Docs grants nothing.
|
|
84
|
+
|
|
85
|
+
## Routes (`/api/plugins/docs`)
|
|
86
|
+
|
|
87
|
+
- `GET /count` (2.7) - sidebar badge: documents you own (Trash left out), `inventory: true`; details rows Yours / Shared with you / Starred / In Trash. Cached a minute per user (a Map; entries older than 60 s are dropped), keyed to a write generation bumped in `onResponse` for every non-GET request - a count stores its answer only if no write finished while it read
|
|
88
|
+
- `GET/PUT /settings` (`autoSaveInterval, maxVersions, publicLinks, allowSend, trashDays`; `scope` read-only; `extensions`)
|
|
89
|
+
- `GET /people` - share picker (id, name; email for admins), filtered to roles holding `docs.read`
|
|
90
|
+
- `GET /documents?view=mine|recent|starred|shared|all|trash&folder=&q=` - rows with `excerpt`, no body
|
|
91
|
+
- `POST /documents`, `POST /documents/import {name, text, folderId}` (5 MB)
|
|
92
|
+
- `GET/PUT/DELETE /documents/:id` (`GET ?poll=1` = a reader's refresh, not recorded as opened; `PUT
|
|
93
|
+
baseUpdatedAt`) (DELETE = Trash; `?permanent=1` from Trash only), `POST .../restore`, `POST /trash/empty`
|
|
94
|
+
- `POST .../duplicate {title?, content?}`, `POST .../star {on}`
|
|
95
|
+
- `POST/DELETE .../lock` (`session`), `PUT .../shares {shares, notify, message}`, `POST/DELETE .../link`
|
|
96
|
+
- `POST .../send {to, message, mode:'inline'|'link'}` - max 10, 10 per 10 min
|
|
97
|
+
- `GET .../versions`, `GET .../versions/:num`, `POST .../versions/:num/restore`
|
|
98
|
+
- `GET/POST /folders`, `PUT/DELETE /folders/:id`, `GET/POST /templates`, `DELETE /templates/:id`
|
|
99
|
+
|
|
100
|
+
## Extension registry (for Docs Pro)
|
|
101
|
+
|
|
102
|
+
`docsRegistry()` - `register(owner, {label, admin, markup, publicGate, linkInfo, on})`, `services`
|
|
103
|
+
(readDoc, writeDoc, listDocs, accessOf, sanitizeDoc, allowMarkup, siteBase, people, config, htmlToText,
|
|
104
|
+
and since 2.1 `listFolders`, `isAdmin` - Docs Pro's Spaces are folders; since 2.2 `convert` {standaloneHtml,
|
|
105
|
+
htmlToMarkdown, htmlToText, fileName, startsWithTitle, parseHtml (2.4)} - Docs Pro's cloud copies and Word files; since 2.3 `setLinkGuard(docId, on)`;
|
|
106
|
+
since 2.4 `createDoc({title, content, folderId}, user)` - sanitised, folder checked with `usableFolder`, into Recent),
|
|
107
|
+
`gate()`, `linkInfo()`, `emit('saved'|'trashed'|'deleted'|'linkRemoved')`. Register from `onReady`.
|
|
108
|
+
The admin screens import each `admin` module and call `install(host)`; the host offers
|
|
109
|
+
`addDocMenu(fn)`, `addShareSection(fn)`, Home `addPlace({key,label,icon,render})`, editor
|
|
110
|
+
`openSide(title)`/`closeSide()`, `doc()`, `reload()`, `save()`, `onDispose`, and since 2.1 `addChip()` (an
|
|
111
|
+
element beside the lock/Trash chips) and `watchDoc(fn)` (called with the document now and on every change).
|
|
112
|
+
Home hides search, sort and grid/list while an add-on place is open (2.1).
|
|
113
|
+
Since 2.4: Home `addImporter({ext: ['.docx'], accept, label, import(file, {folderId}) => doc})` (New > Import and drop
|
|
114
|
+
use it for matching files; extends the file input's `accept`), both screens `allowAttrs([...])` (more attributes kept by the
|
|
115
|
+
admin's DOMPurify - Pro's `data-comment`; the SERVER side is the registry's `markup`), and the editor's DocEditor
|
|
116
|
+
`saveRange()`, `restoreRange(r)`, `selectedText(r)`, `insertHtml(html, r)`, `applyNamed(name, r)` (2.8 - a named
|
|
117
|
+
toolbar action at a saved selection: bold/italic/underline/strike/highlight/inlineCode/link/clearFormat/bulletList/
|
|
118
|
+
numberedList/checklist/quote; refused read-only and while suggesting), `wrapSelection({data-*}, r)` (a span; across
|
|
119
|
+
blocks keeps the first block's part), `unwrap(selector)`. "Use a template" sets `sessionStorage['docs:fill-next']='1'` when
|
|
120
|
+
the template holds `{{...}}` - Pro's merge fields open their fill-in panel.
|
|
121
|
+
|
|
122
|
+
## Admin traps met here
|
|
123
|
+
|
|
124
|
+
- `data-each` templates are set in JS; whole class lists are one `{{cls}}` (whitespace next to
|
|
125
|
+
`{{…}}` is dropped); a select's value is set after render.
|
|
126
|
+
- Markdown shortcuts are DOM work: `execCommand('insertUnorderedList')` in a freshly emptied `<p>`
|
|
127
|
+
nests the list inside the `<p>`.
|
|
128
|
+
- Headless tests: one browser context per user (a shared context shares localStorage and so the
|
|
129
|
+
sign-in); Domma keeps a hidden `.dm-dialog-confirm` - click the visible one.
|
|
130
|
+
|
|
131
|
+
## Tests
|
|
132
|
+
|
|
133
|
+
`tests/plugin.test.js` (API: settings, versions, permissions, sanitising, sharing, notify, lock, trash,
|
|
134
|
+
star/recent, public links + page, import, send, people - mail and users are injected via
|
|
135
|
+
`options.mailer` / `options.listUsers`), `tests/convert.test.js`, `tests/shortcuts.test.js`. Run from
|
|
136
|
+
the CMS: `cd cms && node --test --test-concurrency=1 ../dcms-marketplace/plugins/docs/tests/*.test.js`.
|
|
137
|
+
|
|
138
|
+
## Suggesters (2.5)
|
|
139
|
+
|
|
140
|
+
Share role `suggester` - `accessOf` answers it; `canEdit` is false. While an extension registers `suggest: {check}` (Docs Pro,
|
|
141
|
+
`registry.hasSuggest()`), a Suggester may take the edit lock and PUT the body: title/tags ignored, folder refused, the lock
|
|
142
|
+
REQUIRED (423 without it), and the body stored is `registry.checkSuggestion(stored, sent, user)` (null -> 403
|
|
143
|
+
`suggestOnly`). Without Pro a Suggester is a Viewer everywhere (no lock, no save) and the share role is kept as it was
|
|
144
|
+
(new ones cannot be given). `/settings` answers `suggestions`. Admin: share panel offers "Suggester" when
|
|
145
|
+
`site.suggestions` (or someone already has it); the editor treats a Suggester as able to edit (then Pro forces
|
|
146
|
+
suggesting mode), title read-only, no version restore. DocEditor `setSuggesting(on)` switches off the toolbar, Markdown
|
|
147
|
+
shortcuts, Tab, Enter-in-checklist, checklist ticks, the table menu, Docs' paste/drop handling and find-and-replace's
|
|
148
|
+
Replace - so an add-on can track every change from `beforeinput`.
|
|
149
|
+
|
|
150
|
+
**What leaves Docs** goes through add-ons' filters: server `registry.publicHtml(html)` (public page and its downloads,
|
|
151
|
+
email "inline") and admin `outgoing(html)` (`setContentFilter(key, fn)` on both screens' host; downloads, print, Send
|
|
152
|
+
to Page / Blog). Docs Pro uses them to strip comment marks and show pending suggestions as not made.
|
|
153
|
+
|
|
154
|
+
## Ready-made templates (2.6)
|
|
155
|
+
|
|
156
|
+
An extension's `templates()` (registry) lists read-only templates: `GET /templates` answers the user's own first, then
|
|
157
|
+
`{id: 'x-<owner>-<id>', name, description, category, content (sanitised), builtIn: true, source: <label>}`. Home shows
|
|
158
|
+
"<category> · <source>" and no Delete. (admin/lib/templates.js `showPicker` is not wired to anything - its built-in
|
|
159
|
+
branch only matters if it is revived.) DELETE on one is a 404
|
|
160
|
+
(they are not in templates.json).
|
|
161
|
+
|
|
162
|
+
## 2.8.1 (2026-10-13): trusted email links; history for editors
|
|
163
|
+
|
|
164
|
+
- `siteBase()` is the Site URL (or old `site.url`) only; `linkBase(req)` (async, in the registry's services) is what
|
|
165
|
+
emails and the share panel's link use: the Site URL, else core's `trustedOrigin` (an address someone has really
|
|
166
|
+
signed in on, as password resets do). Never the request's Host header - an insider could send colleagues a link to a
|
|
167
|
+
lookalike host. With no trusted address the share email names the path and the log warns.
|
|
168
|
+
- `GET .../versions` and `.../versions/:num` answer 403 to anyone who cannot edit (viewers, suggesters, `scope:'all'`
|
|
169
|
+
readers): a version may hold text the owner removed before sharing. The editor menu shows Version history to
|
|
170
|
+
editors and owners only.
|
|
171
|
+
|
|
172
|
+
## The Tool Upgrade (2.9.0, 2026-10-14)
|
|
173
|
+
|
|
174
|
+
- **Home banner:** New (+, the New menu is bound to that button) and Import beside the cog. The rail's own New button is
|
|
175
|
+
hidden when the banner exists (kept for a core without the chrome). A help mark beside the list title explains the places.
|
|
176
|
+
- **Shift+F10** on a focused document card, template card or folder opens its menu; every Home menu has a header.
|
|
177
|
+
- **Sidebar badge:** `recount()` (kit.sidebarCount - added to loadKit) after create, import, copy, trash, restore,
|
|
178
|
+
delete for good and Empty Trash.
|
|
179
|
+
- **Unsaved work:** the editor's existing `beforeunload` (draft + browser prompt when dirty) now also asks while a save is
|
|
180
|
+
on its way. Share, Email and Settings ask before closing with a change (`admin/lib/panel-guard.js` guardClose, applied
|
|
181
|
+
AFTER kit.onceClosed - otherwise onceClosed's wrapper tears the panel down whatever the question answered).
|
|
182
|
+
- **The editor keeps its own top bar** for Outline, Find, History, Focus, Share and More - Focus mode hides the admin
|
|
183
|
+
banner (`body.docs-focus .dm-plugin-banner`), so document actions in the banner would vanish exactly when writing.
|
|
184
|
+
- Prompts with a starting value use `promptWith(message, options, value)` (api.js), never E.prompt's `inputValue`: Domma
|
|
185
|
+
sanitises that into `value="…"` and turns `"` back into a bare quote, so a title with `"` opened cut short (escaping
|
|
186
|
+
it does not help). The value is set as a property on the dialog's input, which exists as soon as E.prompt returns.
|
|
187
|
+
|
|
188
|
+
## 2.10.0 (2026-10-15 Tool Upgrade, second pass)
|
|
189
|
+
|
|
190
|
+
- Drafts: `admin/lib/drafts.js` (pure, tested), stored as `docs.draft.<userId>.<docId>` = `{owner, title, content, at,
|
|
191
|
+
was}`. Another person's draft is never read back; 2.9's drafts with no owner are dropped. Restore question: OK throws
|
|
192
|
+
away, Cancel/Escape put back, or make a new document "<title> (your unsaved changes)" if the original is gone. Enter
|
|
193
|
+
is ignored and the page locked while it asks; a draft goes only after an answer.
|
|
194
|
+
- `panel-guard.js`: `claimPanel` (one of a kind), `tieToScreen`, `guardClose(..., busy)`. Share and Settings keep values
|
|
195
|
+
typed during a slow save; Email ignores close while sending.
|
|
196
|
+
- `/count` answers `places` {starred, shared, mine (top level), trash} for the rail. Its cache generation is
|
|
197
|
+
`runtime.writeGen` in lib/store.js, bumped by EVERY store write (Docs Pro writes through services, not Docs' routes).
|
|
198
|
+
- Menus on Share people and Version history rows.
|
|
199
|
+
|
|
200
|
+
## 2.11.0 (2026-10-15): the Wiki's hooks
|
|
201
|
+
|
|
202
|
+
- Registry `access: (doc, user) => 'editor' | 'viewer' | null` (synchronous) - `extraAccess()` gives the strongest any
|
|
203
|
+
extension grants. `store.accessOf` takes the stronger of the share/site-wide role and the extension's (owner stays
|
|
204
|
+
owner). The Wiki makes `wiki.update` holders editors of every wiki page.
|
|
205
|
+
- `onlyThroughExtension(doc, user)`: a document reached ONLY through an extension (not owned, not shared, not
|
|
206
|
+
site-wide) is in no Home place but Starred (when starred) and Recent (when opened) - it was never shared with them.
|
|
207
|
+
- Services: `mutateDoc(id, fn)`, `publicHtml(html)` (= `registry.publicHtml(withoutSuggestions(sanitizeDoc(html)))`,
|
|
208
|
+
what the public sees), `lockOf(id)` (the lock as `publicLock` shows it to nobody's session, or null).
|
|
209
|
+
- Access ONLY through an extension is for editing and history: `outwardRefusal(doc, user)` - PUT shares, POST/DELETE
|
|
210
|
+
link and send answer 403; the editor hides Share and Send to, Home hides Share/Email (`row.outward`).
|
|
211
|
+
- Registry `restrict: (doc) => null | {reason}` (`restrictionOf`, a throwing one restricts): for EVERYONE, owner
|
|
212
|
+
included - POST link and send refused (403, the reason in `outwardRefusal`), the public page `/docs/s/:token` 404s
|
|
213
|
+
(an existing link stops working while restricted, and works again after), the share panel shows the reason instead
|
|
214
|
+
of the link switch, Send to is hidden. PUT shares refuses (403) any change that ADDS someone, owner included -
|
|
215
|
+
existing shares keep working and removing people is allowed; the share panel hides Add people and says why (`restricted` on the document). The Wiki restricts its pages
|
|
216
|
+
while it is members-only. (Send to page/blog is browser-side through core APIs: hidden, not enforced.)
|
|
217
|
+
- Events `restored(doc, user, num)` after a version restore, `untrashed(doc, user)` after a restore from Trash.
|
|
218
|
+
- Tested through the Wiki's API tests (`plugins/wiki/tests/api.test.js`) with real Docs underneath.
|
|
219
|
+
|
|
220
|
+
## 2.12.0 (2026-10-15): freeze - protection that reaches Docs' editor
|
|
221
|
+
|
|
222
|
+
- Registry `freeze: (doc, user) => null | {reason}` (synchronous; `frozenFor(doc, user)` - the first reason, a throwing
|
|
223
|
+
one freezes) and `store.frozenFor()`. For EVERYONE, owner included: PUT of title/content -> 403 `{error: reason,
|
|
224
|
+
frozen: true}` (folder, tags still allowed), `POST /lock` -> `{ok: false, readOnly: true, frozen}`, version restore
|
|
225
|
+
and DELETE (Trash or permanent) -> 403. Reading, history, duplicating and the rest are untouched. `full()` answers
|
|
226
|
+
`frozen` (the reason, '' when not); the editor treats a frozen document as not editable (`mayEdit`) and shows a
|
|
227
|
+
"Protected" chip with the reason as its title.
|
|
228
|
+
- The Wiki (0.2) registers it from its add-ons' `canEdit` (Wiki Pro's protection, locks and a wiki's editor roles).
|
|
229
|
+
`typeof registry.frozenFor === 'function'` is how a consumer detects 2.12 (the Wiki's `/status.protectable`).
|
|
230
|
+
- A frozen document also refuses `POST .../restore` (out of Trash), and `POST /trash/empty` leaves it in Trash.
|
|
231
|
+
- Service `outwardRefusal(doc, user)` - why this person may not share, link, send or publish it ('' = they may);
|
|
232
|
+
Docs Pro 0.9.1 refuses to publish such a document to a Space.
|
|
233
|
+
- Credited saves (for Wiki Pro's approved suggestions): service `snapshotVersion(doc, by)` keeps what a change replaces
|
|
234
|
+
as a version, as a save does. A document's `creditName` + `creditAt` (equal to its `updatedAt`) names who that save
|
|
235
|
+
is credited to ("Sam (visitor)"): the version made of it carries `savedByName`, and `shouldSnapshot` never folds a
|
|
236
|
+
credited save into the next one. Both apply only while `creditAt === updatedAt` - the next ordinary save ends it.
|
|
237
|
+
|
|
238
|
+
## 2.13.0 (2026-10-20): PDF documents
|
|
239
|
+
|
|
240
|
+
A document can be a file. `format: 'pdf'` + `file: {name, size, pages, sha256, encrypted, uploadedAt, uploadedBy}`
|
|
241
|
+
(`fileRecord()`, whitelisted); the bytes at `data/files/<id>.pdf`; `content` is the TEXT READ OUT OF THE FILE as
|
|
242
|
+
`<p>` HTML (`extractToHtml`, sanitised), so search, the card excerpt and `wordCount` work unchanged. `kindOf(doc)` -
|
|
243
|
+
`'pdf' | 'html'` - is the one question; everything that renders, exports or edits a body asks it. Docs never rewrites
|
|
244
|
+
a PDF: it is kept whole or refused.
|
|
245
|
+
|
|
246
|
+
- **Checks (`lib/pdf.js inspectPdf`)**: 20 MB cap (`MAX_PDF_BYTES`), `%PDF-` in the first KB, then every name in
|
|
247
|
+
`REFUSED_NAMES` (JavaScript, JS, Launch, EmbeddedFile(s), RichMedia, XFA, GoToE, SubmitForm, ImportData) searched in
|
|
248
|
+
the raw bytes AND every FlateDecode stream inflated (per-stream and total caps - a bomb is skipped, not followed), with
|
|
249
|
+
`#xx` name escapes undone first. Refused with the plain reason, never stripped. `/OpenAction` is NOT refused (a plain
|
|
250
|
+
"open at page 1" is in every LaTeX export; a script behind it is caught by /JS). Encrypted files are kept (no extract).
|
|
251
|
+
- **Text extract**: `extractPdfText` runs pdfjs-dist (`pdfjs-dist/legacy/build/pdf.mjs`, an OPTIONAL dependency of the
|
|
252
|
+
CMS - `extractCapability()` resolves it without loading) in a `worker_threads` Worker with scripting off, no fonts,
|
|
253
|
+
300 pages / 200k chars, and a 20 s budget - over time it is terminated and the document stored without an extract.
|
|
254
|
+
`GET /settings` answers `pdf: {maxBytes, search, reason}`. Tests skip the extract test where pdfjs is absent.
|
|
255
|
+
- **Routes**: `POST /documents/upload {name, data: base64, folderId, title?}` (the same road as Docs Pro's Word import -
|
|
256
|
+
no multipart dependency), `GET /documents/:id/file` (inline; `?download=1` saves; anyone who may read),
|
|
257
|
+
`PUT /documents/:id/file {name, data}` (editors; ALWAYS a version - a new upload is a real change; 409 in Trash, 423
|
|
258
|
+
while another session holds the lock, 403 frozen), `GET /documents/:id/versions/:num/file` (editors, as history is),
|
|
259
|
+
`GET /documents/:id/view?t=<ticket>` (below). PUT of `content` on a PDF -> 400; title/folder/tags still save and keep
|
|
260
|
+
`format`. Duplicate copies the file BEFORE the record is written. Restore of a PDF version copies `<n>.pdf` back.
|
|
261
|
+
`removeDoc` removes the file; the version trim removes `<n>.pdf` with `<n>.json`. Send "inline" attaches the PDF.
|
|
262
|
+
- **The file goes out** (`sendPdf`) with `application/pdf`, `Content-Disposition` from the TITLE (never the uploaded
|
|
263
|
+
name), `X-Content-Type-Options: nosniff`, `Cache-Control: private, no-store`, `Content-Security-Policy: default-src
|
|
264
|
+
'none'; sandbox`, `X-Robots-Tag: noindex`. The public page frames `/docs/s/<token>?file=1` and offers `?download=pdf`;
|
|
265
|
+
`?download=md|html|txt` on a PDF answers the PDF (nothing else is derived from it). `public-doc.html` takes its action
|
|
266
|
+
links as `{{actions}}` now.
|
|
267
|
+
- **View tickets - why the admin frame is not a blob**. The admin's CSP has `frame-src 'self' …`: a `blob:` frame is
|
|
268
|
+
REFUSED (the first build failed exactly there). An `<iframe>` carries no bearer header either. So `full()` answers
|
|
269
|
+
`fileTicket` for a PDF - HMAC-SHA256 of `{d: docId, u: userId, a: admin, e: expiry}` under a key made at boot
|
|
270
|
+
(`viewKey`), 20 minutes - and the frame's address is `/view?t=…` (no `authenticate`; the ticket names the person
|
|
271
|
+
and the route checks `accessOf` for them again). The editor polls every 30 s, so `current.fileTicket` is always
|
|
272
|
+
fresh; a restart ends every ticket and the next poll hands out new ones.
|
|
273
|
+
- **No `sandbox` ATTRIBUTE on the frame**: Chrome's PDF viewer will not open in a sandboxed iframe (`chrome-error://`
|
|
274
|
+
instead - measured). The CSP `sandbox` header on the response is fine (the viewer loads, measured too).
|
|
275
|
+
- **Registry `kinds`** (`register(owner, {kinds: ['html', 'pdf']})`, default `['html']`): an add-on written before PDF
|
|
276
|
+
documents sees none of them - `emit()` skips it for a PDF (first arg with `format`), `adminModules()` carries `kinds`,
|
|
277
|
+
Home tags each add-on's `addDocMenu`/`addShareSection` function with the installing add-on's kinds (`forKind`) and the
|
|
278
|
+
editor installs only add-ons declaring `'pdf'` in a PDF's screen. `registry.handles(ext, doc)`. SERVICES ARE NOT
|
|
279
|
+
FILTERED: Docs Pro < 0.10 that lists documents itself (Spaces' folder publish, cloud "every save" runs through
|
|
280
|
+
`on.saved` - skipped; but `listDocs` in a Space) sees the text extract as the body. Docs Pro 0.10 branches on `kindOf`.
|
|
281
|
+
Services added: `kindOf`, `readFile(docId)`, `writeFile(docId, buf, user, {name})` (= PUT .../file), `pdfCapability()`.
|
|
282
|
+
- **Admin**: Home routes `.pdf` files (`isPdfFile`) to `uploadPdf` (`readBase64`), cards say `PDF · n pages · size`
|
|
283
|
+
(`pdfFacts`) with a PDF chip, Download offers the PDF only (`pdfExportItems`). The editor for a PDF: the DocEditor
|
|
284
|
+
runs on a DETACHED element (its save/lock/outline paths stay inert) and `mountPdfViewer` has the real `.docs-page`;
|
|
285
|
+
`mayEdit()` is false (mode `view`, no lock, no drafts), the title is renamed from the menu (saved on its own),
|
|
286
|
+
`pdfMenu()` replaces `moreMenu()` (Rename, copy, move, Download PDF, Replace the PDF…, Send to > Email, history,
|
|
287
|
+
add-ons, Trash). `showContent()` for a PDF reloads the viewer when `file.sha256`/`uploadedAt` changed
|
|
288
|
+
(`fresh._before` carries the record as it was). Version history rows/preview show facts + Download this version.
|
|
289
|
+
`.docs-ed.is-pdf` hides the toolbar, outline, Outline and Find buttons (CSS).
|
|
290
|
+
- Tests: `tests/pdf.test.js` (lib), four `2.13:` tests in `tests/plugin.test.js`; `tests/_pdf.js` makes real PDFs
|
|
291
|
+
(xref correct, optional FlateDecode, extra objects for the refusals). Browser-checked headlessly: upload -> editor ->
|
|
292
|
+
viewer loads (Chrome's PDF viewer frame present) -> replace -> history -> Home card -> public page.
|
|
293
|
+
|
|
294
|
+
## 2.13.1 (2026-10-20): `services.createPdfDoc`
|
|
295
|
+
|
|
296
|
+
`createPdfDoc({title, buf, name, folderId}, user)` - a PDF document from bytes, as the upload route makes one (checked
|
|
297
|
+
by `inspectPdf`, text read, filed where the person may file, into their Recent). Throws `{status, message}`. Docs Pro
|
|
298
|
+
0.11's "take pages out" is the caller.
|
|
299
|
+
|
|
300
|
+
## 2.13.2 (2026-10-20): review fixes
|
|
301
|
+
|
|
302
|
+
- **The name scan inflates every stream** (`lib/pdf.js searchableText`): no dictionary is parsed to decide - the first build
|
|
303
|
+
read the innermost `<<` before `stream`, so any stream whose dictionary held a nested one (every object stream with
|
|
304
|
+
/DecodeParms) was never inflated and a /JavaScript or /EmbeddedFile inside it passed. Each body that starts with a zlib
|
|
305
|
+
header is inflated, again while the output still looks deflated (double /FlateDecode, 3 deep), and a stream past the cap
|
|
306
|
+
(32 MB a stream, 160 MB in all) makes the file UNCHECKABLE and refused ("too large to check"), not skipped.
|
|
307
|
+
- **Restoring the oldest PDF version at the history cap** read its file after the restore's own snapshot had trimmed it:
|
|
308
|
+
the version's bytes are read BEFORE the snapshot now, and written with `writeFileBytes`.
|
|
309
|
+
- `POST /documents` passes only title/content/folderId/tags to `newDoc` - a record saying `format: 'pdf'` with no file
|
|
310
|
+
behind it can no longer be made there.
|
|
311
|
+
- `GET /documents/:id/view` re-checks that the ticket's person still exists, is active and holds a role with `docs.read`
|
|
312
|
+
(or level 0) - a ticket no longer outlives the permission for its 20 minutes.
|
|
313
|
+
|
|
314
|
+
## 2.14.0 (2026-10-21): free
|
|
315
|
+
|
|
316
|
+
`tier: free`, no `closedSource` - no licence is needed or checked any more (the engine only checks a
|
|
317
|
+
`closedSource` plugin). Included in every hosted plan and free to self-host; not `bundle: false`, so the next
|
|
318
|
+
`make sync-free-tier` ships it in the engine. Docs Pro and Wiki Pro stay paid and still `requires: ["docs"]`.
|