domma-cms 0.55.1 → 0.69.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/CLAUDE.md +194 -3
  2. package/README.md +18 -14
  3. package/admin/css/admin.css +1 -1
  4. package/admin/css/contacts.css +1 -0
  5. package/admin/dist/domma/domma-tools.css +3 -3
  6. package/admin/dist/domma/domma-tools.min.js +3 -3
  7. package/admin/index.html +1 -0
  8. package/admin/js/api.js +1 -1
  9. package/admin/js/app.js +4 -4
  10. package/admin/js/lib/analytics-shape.js +1 -0
  11. package/admin/js/lib/contacts-arrange.js +1 -0
  12. package/admin/js/lib/help-popover.js +1 -0
  13. package/admin/js/lib/notes-arrange.js +1 -0
  14. package/admin/js/lib/page-picker.js +1 -0
  15. package/admin/js/lib/plugin-accent.js +1 -0
  16. package/admin/js/lib/plugin-chrome.js +1 -0
  17. package/admin/js/lib/shortcode-context-menu.js +2 -2
  18. package/admin/js/lib/sidebar-grouping.js +1 -1
  19. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  20. package/admin/js/lib/sidebar-renderer.js +4 -4
  21. package/admin/js/lib/slideover-resizable.js +1 -0
  22. package/admin/js/lib/todo-arrange.js +1 -0
  23. package/admin/js/lib/tool-kit.js +1 -0
  24. package/admin/js/templates/analytics.html +138 -0
  25. package/admin/js/templates/contacts.html +156 -0
  26. package/admin/js/templates/context-menu-editor.html +212 -0
  27. package/admin/js/templates/context-menus.html +16 -0
  28. package/admin/js/templates/notes.html +137 -0
  29. package/admin/js/templates/plugin-code.html +1 -1
  30. package/admin/js/templates/plugin-marketplace.html +17 -4
  31. package/admin/js/templates/plugins.html +13 -7
  32. package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
  33. package/admin/js/templates/settings.html +16 -97
  34. package/admin/js/templates/theme.html +211 -0
  35. package/admin/js/templates/todo.html +113 -0
  36. package/admin/js/views/analytics.js +62 -0
  37. package/admin/js/views/contacts.js +174 -0
  38. package/admin/js/views/context-menu-editor.js +55 -0
  39. package/admin/js/views/context-menus.js +5 -0
  40. package/admin/js/views/form-editor.js +7 -7
  41. package/admin/js/views/index.js +1 -1
  42. package/admin/js/views/notes.js +104 -0
  43. package/admin/js/views/plugin-marketplace.js +1 -1
  44. package/admin/js/views/plugins.js +28 -24
  45. package/admin/js/views/search.js +1 -0
  46. package/admin/js/views/settings.js +3 -3
  47. package/admin/js/views/theme.js +1 -0
  48. package/admin/js/views/todo.js +96 -0
  49. package/bin/cli.js +6 -10
  50. package/bin/lib/plugin-version.js +28 -0
  51. package/bin/update.js +27 -2
  52. package/config/menus/admin-sidebar.json +129 -23
  53. package/config/plugins.json +5 -5
  54. package/config/search.json +13 -0
  55. package/config/server.json +3 -1
  56. package/package.json +14 -10
  57. package/plugins/_lib/admin/mail/compose-window.js +914 -0
  58. package/plugins/_lib/admin/mail/contacts.js +301 -0
  59. package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
  60. package/plugins/_lib/admin/mail/folder-tree.js +254 -0
  61. package/plugins/_lib/admin/mail/identity.js +480 -0
  62. package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
  63. package/plugins/_lib/admin/mail/keyboard.js +136 -0
  64. package/plugins/_lib/admin/mail/mail.css +1 -0
  65. package/plugins/_lib/admin/mail/mail.html +71 -0
  66. package/plugins/_lib/admin/mail/panes.js +253 -0
  67. package/plugins/_lib/admin/mail/reader-view.js +4473 -0
  68. package/plugins/_lib/admin/mail/resizable.js +26 -0
  69. package/plugins/_lib/admin/mail/rules.js +343 -0
  70. package/plugins/_lib/admin/mail/scheduling.js +203 -0
  71. package/plugins/_lib/admin/mail/section-kit.js +277 -0
  72. package/plugins/_lib/admin/mail/templates.js +238 -0
  73. package/plugins/_lib/admin/mail/threads.js +200 -0
  74. package/plugins/_lib/admin/mail/vacation.js +269 -0
  75. package/plugins/_lib/admin/ui/help.css +1 -0
  76. package/plugins/_lib/admin/ui/help.js +174 -0
  77. package/plugins/_lib/admin/ui/resizable.js +151 -0
  78. package/plugins/_lib/dataStore.js +117 -0
  79. package/plugins/_lib/mail/accounts.js +919 -0
  80. package/plugins/_lib/mail/bodyTokens.js +101 -0
  81. package/plugins/_lib/mail/compose.js +256 -0
  82. package/plugins/_lib/mail/defaults.js +57 -0
  83. package/plugins/_lib/mail/diagnostics.js +154 -0
  84. package/plugins/_lib/mail/envelope.js +161 -0
  85. package/plugins/_lib/mail/folders.js +192 -0
  86. package/plugins/_lib/mail/handoff.js +274 -0
  87. package/plugins/_lib/mail/imapPool.js +293 -0
  88. package/plugins/_lib/mail/mbox.js +74 -0
  89. package/plugins/_lib/mail/pollSchedule.js +79 -0
  90. package/plugins/_lib/mail/poller.js +135 -0
  91. package/plugins/_lib/mail/priority.js +138 -0
  92. package/plugins/_lib/mail/readRoutes.js +680 -0
  93. package/plugins/_lib/mail/render.js +291 -0
  94. package/plugins/_lib/mail/ruleRunner.js +152 -0
  95. package/plugins/_lib/mail/scheduler.js +254 -0
  96. package/plugins/_lib/mail/secretbox.js +229 -0
  97. package/plugins/_lib/mail/send.js +396 -0
  98. package/plugins/_lib/mail/store.js +1022 -0
  99. package/plugins/_lib/mail/sync.js +292 -0
  100. package/plugins/_lib/mail/syncPlan.js +126 -0
  101. package/plugins/_lib/mail/syncSelection.js +82 -0
  102. package/plugins/_lib/mail/unsubscribe.js +183 -0
  103. package/plugins/_lib/mail/vacationRunner.js +114 -0
  104. package/plugins/_lib/mail/write.js +473 -0
  105. package/plugins/_lib/schemaSync.js +83 -0
  106. package/plugins/_template/admin/css/index.css +0 -0
  107. package/plugins/_template/admin/templates/index.html +4 -4
  108. package/plugins/_template/admin/views/index.js +7 -0
  109. package/plugins/blog/CLAUDE.md +237 -0
  110. package/plugins/blog/admin/css/index.css +178 -0
  111. package/plugins/blog/admin/templates/blog.html +35 -41
  112. package/plugins/blog/admin/templates/categories.html +19 -6
  113. package/plugins/blog/admin/templates/comments.html +26 -9
  114. package/plugins/blog/admin/templates/post-editor.html +63 -40
  115. package/plugins/blog/admin/templates/settings.html +3 -9
  116. package/plugins/blog/admin/views/blog.js +288 -159
  117. package/plugins/blog/admin/views/categories.js +212 -206
  118. package/plugins/blog/admin/views/comments.js +207 -158
  119. package/plugins/blog/admin/views/kit.js +372 -0
  120. package/plugins/blog/admin/views/post-editor.js +306 -39
  121. package/plugins/blog/admin/views/settings-cog.js +56 -0
  122. package/plugins/blog/admin/views/settings.js +16 -84
  123. package/plugins/blog/blocks/blog-card-compact.css +17 -0
  124. package/plugins/blog/blocks/blog-card-compact.html +8 -0
  125. package/plugins/blog/blocks/blog-card-row.css +22 -0
  126. package/plugins/blog/blocks/blog-card-row.html +11 -0
  127. package/plugins/blog/blocks/blog-card.css +35 -0
  128. package/plugins/blog/blocks/blog-card.html +16 -0
  129. package/plugins/blog/blocks/blog-post-classic.css +15 -0
  130. package/plugins/blog/blocks/blog-post-classic.html +28 -0
  131. package/plugins/blog/blocks/blog-post-feature.css +20 -0
  132. package/plugins/blog/blocks/blog-post-feature.html +30 -0
  133. package/plugins/blog/blocks/blog-post-minimal.css +10 -0
  134. package/plugins/blog/blocks/blog-post-minimal.html +23 -0
  135. package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
  136. package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
  137. package/plugins/blog/collections/categories/schema.json +7 -6
  138. package/plugins/blog/collections/comments/schema.json +11 -10
  139. package/plugins/blog/collections/posts/schema.json +16 -13
  140. package/plugins/blog/config.js +6 -0
  141. package/plugins/blog/lib/layouts.js +304 -0
  142. package/plugins/blog/lib/page.js +158 -0
  143. package/plugins/blog/lib/render.js +119 -0
  144. package/plugins/blog/lib/samples.js +234 -0
  145. package/plugins/blog/plugin.js +259 -26
  146. package/plugins/blog/plugin.json +21 -73
  147. package/plugins/blog/plugin.public.js +217 -0
  148. package/plugins/blog/public/blog.css +177 -0
  149. package/plugins/blog/public/blog.js +586 -0
  150. package/plugins/blog/public/samples/aurora.svg +7 -0
  151. package/plugins/blog/public/samples/dusk.svg +7 -0
  152. package/plugins/blog/public/samples/ember.svg +7 -0
  153. package/plugins/blog/public/samples/harbour.svg +7 -0
  154. package/plugins/blog/public/samples/meadow.svg +7 -0
  155. package/plugins/blog/public/samples/rosewood.svg +7 -0
  156. package/plugins/blog/templates/index.html +9 -8
  157. package/plugins/blog/templates/post.html +4 -17
  158. package/plugins/blog/tests/layouts.test.js +97 -0
  159. package/plugins/blog/tests/public.test.js +71 -0
  160. package/plugins/free-tier.lock.json +74 -0
  161. package/plugins/mail-reader/CLAUDE.md +78 -0
  162. package/plugins/mail-reader/admin/views/mail.js +19 -0
  163. package/plugins/mail-reader/config.js +7 -0
  164. package/plugins/mail-reader/plugin.js +48 -0
  165. package/plugins/mail-reader/plugin.json +34 -0
  166. package/public/css/forms.css +1 -1
  167. package/public/css/search.css +1 -0
  168. package/public/css/site.css +1 -1
  169. package/public/css/theme-switcher.css +1 -0
  170. package/public/js/analytics.js +1 -0
  171. package/public/js/collection-context.js +2 -2
  172. package/public/js/context-menus.js +1 -0
  173. package/public/js/form-logic-engine.js +1 -1
  174. package/public/js/forms.js +2 -2
  175. package/public/js/search.js +1 -0
  176. package/public/js/site.js +1 -1
  177. package/public/js/theme-switcher.js +1 -0
  178. package/scripts/build.js +77 -4
  179. package/scripts/copy-domma.js +48 -0
  180. package/scripts/run-tests.mjs +92 -0
  181. package/scripts/seed.js +1996 -0
  182. package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
  183. package/server/routes/api/collections.js +34 -0
  184. package/server/routes/api/contacts.js +506 -0
  185. package/server/routes/api/context-menus.js +104 -0
  186. package/server/routes/api/dashboard.js +8 -2
  187. package/server/routes/api/forms.js +42 -3
  188. package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
  189. package/server/routes/api/notifications.js +69 -19
  190. package/server/routes/api/plugin-marketplace.js +78 -23
  191. package/server/routes/api/plugins.js +198 -9
  192. package/server/routes/api/search.js +43 -0
  193. package/server/routes/api/theme.js +69 -0
  194. package/server/routes/api/todo.js +178 -0
  195. package/server/routes/public.js +42 -7
  196. package/server/server.js +135 -1
  197. package/server/services/adapters/FileAdapter.js +6 -1
  198. package/server/services/collections.js +13 -3
  199. package/server/services/content.js +26 -0
  200. package/server/services/contextMenus.js +477 -0
  201. package/server/services/managerClient.js +72 -136
  202. package/server/services/markdown.js +124 -10
  203. package/server/services/permissionRegistry.js +74 -0
  204. package/server/services/pluginEntitlement.js +171 -0
  205. package/server/services/pluginEnvelope.js +242 -0
  206. package/server/services/pluginFiles.js +52 -11
  207. package/server/services/pluginInstaller.js +246 -26
  208. package/server/services/pluginScaffold.js +5 -0
  209. package/server/services/plugins.js +340 -7
  210. package/server/services/presetCollections.js +344 -0
  211. package/server/services/promoted-plugins-migration.js +254 -0
  212. package/server/services/publicCors.js +46 -0
  213. package/server/services/renderer.js +224 -22
  214. package/server/services/roles.js +1 -1
  215. package/server/services/search-migration.js +82 -0
  216. package/server/services/search.js +413 -0
  217. package/server/services/sidebar-migration.js +1 -0
  218. package/server/services/sidebarBadges.js +37 -0
  219. package/server/services/siteGitignore.js +307 -0
  220. package/server/services/themeSettings.js +580 -0
  221. package/server/services/users.js +8 -0
  222. package/server/templates/page.html +4 -2
  223. package/plugins/analytics/admin/templates/analytics.html +0 -61
  224. package/plugins/analytics/admin/views/analytics.js +0 -176
  225. package/plugins/analytics/config.js +0 -14
  226. package/plugins/analytics/plugin.json +0 -57
  227. package/plugins/analytics/public/inject-body.html +0 -60
  228. package/plugins/analytics/public/inject-head.html +0 -1
  229. package/plugins/blog/templates/author.html +0 -9
  230. package/plugins/blog/templates/category.html +0 -9
  231. package/plugins/blog/templates/tag.html +0 -9
  232. package/plugins/contacts/admin/templates/contacts.html +0 -126
  233. package/plugins/contacts/admin/views/contacts.js +0 -710
  234. package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
  235. package/plugins/contacts/collections/user-contacts/schema.json +0 -71
  236. package/plugins/contacts/config.js +0 -6
  237. package/plugins/contacts/data/contacts.json +0 -20
  238. package/plugins/contacts/plugin.js +0 -291
  239. package/plugins/contacts/plugin.json +0 -33
  240. package/plugins/demo-viewer/config.js +0 -4
  241. package/plugins/demo-viewer/plugin.js +0 -36
  242. package/plugins/demo-viewer/plugin.json +0 -9
  243. package/plugins/invoice/admin/templates/editor.html +0 -129
  244. package/plugins/invoice/admin/templates/index.html +0 -43
  245. package/plugins/invoice/admin/templates/issuers.html +0 -5
  246. package/plugins/invoice/admin/templates/receivers.html +0 -5
  247. package/plugins/invoice/admin/views/editor.js +0 -267
  248. package/plugins/invoice/admin/views/index.js +0 -155
  249. package/plugins/invoice/admin/views/issuers.js +0 -23
  250. package/plugins/invoice/admin/views/party-view.js +0 -148
  251. package/plugins/invoice/admin/views/receivers.js +0 -22
  252. package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
  253. package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
  254. package/plugins/invoice/collections/invoices/schema.json +0 -27
  255. package/plugins/invoice/config.js +0 -16
  256. package/plugins/invoice/plugin.js +0 -283
  257. package/plugins/invoice/plugin.json +0 -85
  258. package/plugins/invoice/templates/invoice-print.html +0 -213
  259. package/plugins/notes/admin/templates/notes.html +0 -83
  260. package/plugins/notes/admin/views/notes.js +0 -281
  261. package/plugins/notes/collections/user-notes/schema.json +0 -54
  262. package/plugins/notes/config.js +0 -6
  263. package/plugins/notes/data/notes.json +0 -1
  264. package/plugins/notes/plugin.json +0 -33
  265. package/plugins/site-search/admin/views/site-search.js +0 -116
  266. package/plugins/site-search/config.js +0 -15
  267. package/plugins/site-search/plugin.js +0 -188
  268. package/plugins/site-search/plugin.json +0 -40
  269. package/plugins/site-search/public/inject-body.html +0 -17
  270. package/plugins/site-search/public/inject-head.html +0 -1
  271. package/plugins/site-search/public/search.css +0 -1
  272. package/plugins/site-search/public/search.js +0 -1
  273. package/plugins/surveys/admin/templates/audience.html +0 -47
  274. package/plugins/surveys/admin/templates/results.html +0 -58
  275. package/plugins/surveys/admin/templates/survey-editor.html +0 -141
  276. package/plugins/surveys/admin/templates/surveys.html +0 -25
  277. package/plugins/surveys/admin/views/audience.js +0 -301
  278. package/plugins/surveys/admin/views/results.js +0 -172
  279. package/plugins/surveys/admin/views/survey-editor.js +0 -211
  280. package/plugins/surveys/admin/views/surveys.js +0 -161
  281. package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
  282. package/plugins/surveys/collections/survey-groups/schema.json +0 -11
  283. package/plugins/surveys/collections/survey-invites/schema.json +0 -16
  284. package/plugins/surveys/collections/surveys/schema.json +0 -23
  285. package/plugins/surveys/config.js +0 -8
  286. package/plugins/surveys/plugin.js +0 -174
  287. package/plugins/surveys/plugin.json +0 -65
  288. package/plugins/surveys/public/dist-shared.mjs +0 -1
  289. package/plugins/surveys/public/survey.css +0 -1
  290. package/plugins/surveys/public/survey.mjs +0 -1
  291. package/plugins/surveys/templates/survey-page.html +0 -63
  292. package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
  293. package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
  294. package/plugins/theme-switcher/config.js +0 -10
  295. package/plugins/theme-switcher/plugin.js +0 -26
  296. package/plugins/theme-switcher/plugin.json +0 -74
  297. package/plugins/theme-switcher/public/inject-body.html +0 -153
  298. package/plugins/theme-switcher/public/inject-head.html +0 -260
  299. package/plugins/todo/admin/templates/todo.html +0 -158
  300. package/plugins/todo/admin/views/todo.js +0 -343
  301. package/plugins/todo/collections/todos/schema.json +0 -60
  302. package/plugins/todo/config.js +0 -7
  303. package/plugins/todo/data/todos.json +0 -1
  304. package/plugins/todo/plugin.js +0 -102
  305. package/plugins/todo/plugin.json +0 -33
@@ -115,6 +115,350 @@ const PRESETS = [
115
115
  delete: {enabled: false, access: 'admin'}
116
116
  }
117
117
  }
118
+ ,
119
+ // ---- Tools promoted out of plugins/ (0.67.0) --------------------------
120
+ // The slugs are what setupPlugin() created from `schema.slug`, NOT the
121
+ // directories the schemas used to live in (`user-notes`, `todos`). Keeping
122
+ // them exactly preserves every existing entry; changing one would seed an
123
+ // empty collection beside a site's real one.
124
+ //
125
+ // seedAll() rewrites schema.json on every boot, so `storage` is declared
126
+ // exactly as the plugins declared it - a site that had these in Mongo must
127
+ // not silently switch to files and lose sight of its rows. Sites with no
128
+ // connections.json fall back to the file adapter as they always did.
129
+ {
130
+ "slug": "notes-notes",
131
+ "title": "Notes",
132
+ "description": "Personal notes, one row per note, scoped to its author.",
133
+ "preset": true,
134
+ "fields": [
135
+ {
136
+ "name": "title",
137
+ "label": "Title",
138
+ "type": "text",
139
+ "required": false
140
+ },
141
+ {
142
+ "name": "content",
143
+ "label": "Content",
144
+ "type": "textarea",
145
+ "required": false
146
+ },
147
+ {
148
+ "name": "categories",
149
+ "label": "Categories",
150
+ "type": "text",
151
+ "required": false
152
+ },
153
+ {
154
+ "name": "userId",
155
+ "label": "User ID",
156
+ "type": "text",
157
+ "required": false
158
+ }
159
+ ],
160
+ "api": {
161
+ "create": {
162
+ "enabled": true,
163
+ "access": "admin"
164
+ },
165
+ "read": {
166
+ "enabled": true,
167
+ "access": "admin"
168
+ },
169
+ "update": {
170
+ "enabled": false,
171
+ "access": "admin"
172
+ },
173
+ "delete": {
174
+ "enabled": false,
175
+ "access": "admin"
176
+ }
177
+ },
178
+ "storage": {
179
+ "adapter": "mongodb",
180
+ "connection": "default"
181
+ }
182
+ },
183
+ {
184
+ "slug": "todo-items",
185
+ "title": "Todo Items",
186
+ "description": "Personal tasks, one row per task, scoped to its owner.",
187
+ "preset": true,
188
+ "fields": [
189
+ {
190
+ "name": "text",
191
+ "label": "Text",
192
+ "type": "text",
193
+ "required": true
194
+ },
195
+ {
196
+ "name": "status",
197
+ "label": "Status",
198
+ "type": "text",
199
+ "required": false
200
+ },
201
+ {
202
+ "name": "priority",
203
+ "label": "Priority",
204
+ "type": "text",
205
+ "required": false
206
+ },
207
+ {
208
+ "name": "dueAt",
209
+ "label": "Due At",
210
+ "type": "text",
211
+ "required": false
212
+ },
213
+ {
214
+ "name": "userId",
215
+ "label": "User ID",
216
+ "type": "text",
217
+ "required": false
218
+ }
219
+ ],
220
+ "api": {
221
+ "create": {
222
+ "enabled": true,
223
+ "access": "admin"
224
+ },
225
+ "read": {
226
+ "enabled": true,
227
+ "access": "admin"
228
+ },
229
+ "update": {
230
+ "enabled": false,
231
+ "access": "admin"
232
+ },
233
+ "delete": {
234
+ "enabled": false,
235
+ "access": "admin"
236
+ }
237
+ },
238
+ "storage": {
239
+ "adapter": "mongodb",
240
+ "connection": "default"
241
+ }
242
+ },
243
+ // ---- Contacts, promoted out of plugins/ (0.67.0) ----------------------
244
+ // Slugs are contacts-contacts and contacts-groups - NOT the directories the
245
+ // schemas lived in (user-contacts, user-contact-groups). Taken verbatim from
246
+ // those files so none of the 25 contact fields is lost in transcription, and
247
+ // `storage` stays `file` exactly as it was.
248
+ {
249
+ "slug": "contacts-contacts",
250
+ "title": "Contacts",
251
+ "description": "Contacts managed by the Contacts plugin.",
252
+ "fields": [
253
+ {
254
+ "name": "name",
255
+ "label": "Name",
256
+ "type": "text",
257
+ "required": true
258
+ },
259
+ {
260
+ "name": "email",
261
+ "label": "Email",
262
+ "type": "text",
263
+ "required": false
264
+ },
265
+ {
266
+ "name": "phone",
267
+ "label": "Phone",
268
+ "type": "text",
269
+ "required": false
270
+ },
271
+ {
272
+ "name": "groups",
273
+ "label": "Groups",
274
+ "type": "text",
275
+ "required": false
276
+ },
277
+ {
278
+ "name": "notes",
279
+ "label": "Notes",
280
+ "type": "textarea",
281
+ "required": false
282
+ },
283
+ {
284
+ "name": "favourite",
285
+ "label": "Favourite",
286
+ "type": "text",
287
+ "required": false
288
+ },
289
+ {
290
+ "name": "company",
291
+ "label": "Company",
292
+ "type": "text",
293
+ "required": false
294
+ },
295
+ {
296
+ "name": "jobTitle",
297
+ "label": "Job Title",
298
+ "type": "text",
299
+ "required": false
300
+ },
301
+ {
302
+ "name": "website",
303
+ "label": "Website",
304
+ "type": "text",
305
+ "required": false
306
+ },
307
+ {
308
+ "name": "vatNumber",
309
+ "label": "VAT Number",
310
+ "type": "text",
311
+ "required": false
312
+ },
313
+ {
314
+ "name": "companyNumber",
315
+ "label": "Company Number",
316
+ "type": "text",
317
+ "required": false
318
+ },
319
+ {
320
+ "name": "reference",
321
+ "label": "Their Reference",
322
+ "type": "text",
323
+ "required": false
324
+ },
325
+ {
326
+ "name": "billingEmail",
327
+ "label": "Billing Email",
328
+ "type": "text",
329
+ "required": false
330
+ },
331
+ {
332
+ "name": "paymentTerms",
333
+ "label": "Payment Terms (days)",
334
+ "type": "number",
335
+ "required": false
336
+ },
337
+ {
338
+ "name": "currency",
339
+ "label": "Preferred Currency",
340
+ "type": "text",
341
+ "required": false
342
+ },
343
+ {
344
+ "name": "addresses",
345
+ "label": "Addresses",
346
+ "type": "json",
347
+ "required": false
348
+ },
349
+ {
350
+ "name": "emails",
351
+ "label": "Additional Emails",
352
+ "type": "json",
353
+ "required": false
354
+ },
355
+ {
356
+ "name": "phones",
357
+ "label": "Additional Phones",
358
+ "type": "json",
359
+ "required": false
360
+ },
361
+ {
362
+ "name": "accountName",
363
+ "label": "Account Name",
364
+ "type": "text",
365
+ "required": false
366
+ },
367
+ {
368
+ "name": "bankName",
369
+ "label": "Bank",
370
+ "type": "text",
371
+ "required": false
372
+ },
373
+ {
374
+ "name": "sortCode",
375
+ "label": "Sort Code",
376
+ "type": "text",
377
+ "required": false
378
+ },
379
+ {
380
+ "name": "accountNumber",
381
+ "label": "Account Number",
382
+ "type": "text",
383
+ "required": false
384
+ },
385
+ {
386
+ "name": "iban",
387
+ "label": "IBAN",
388
+ "type": "text",
389
+ "required": false
390
+ },
391
+ {
392
+ "name": "bic",
393
+ "label": "BIC / SWIFT",
394
+ "type": "text",
395
+ "required": false
396
+ },
397
+ {
398
+ "name": "userId",
399
+ "label": "User ID",
400
+ "type": "text",
401
+ "required": false
402
+ }
403
+ ],
404
+ "api": {
405
+ "create": {
406
+ "enabled": true,
407
+ "access": "admin"
408
+ },
409
+ "read": {
410
+ "enabled": true,
411
+ "access": "admin"
412
+ },
413
+ "update": {
414
+ "enabled": false,
415
+ "access": "admin"
416
+ },
417
+ "delete": {
418
+ "enabled": false,
419
+ "access": "admin"
420
+ }
421
+ },
422
+ "storage": {
423
+ "adapter": "file"
424
+ },
425
+ "preset": true
426
+ },
427
+ {
428
+ "slug": "contacts-groups",
429
+ "title": "Contact Groups",
430
+ "description": "Groups for the Contacts plugin.",
431
+ "fields": [
432
+ {
433
+ "name": "name",
434
+ "label": "Group Name",
435
+ "type": "text",
436
+ "required": true
437
+ }
438
+ ],
439
+ "api": {
440
+ "create": {
441
+ "enabled": true,
442
+ "access": "admin"
443
+ },
444
+ "read": {
445
+ "enabled": true,
446
+ "access": "admin"
447
+ },
448
+ "update": {
449
+ "enabled": false,
450
+ "access": "admin"
451
+ },
452
+ "delete": {
453
+ "enabled": false,
454
+ "access": "admin"
455
+ }
456
+ },
457
+ "storage": {
458
+ "adapter": "file"
459
+ },
460
+ "preset": true
461
+ }
118
462
  ];
119
463
 
120
464
  /** Slugs exported for use in adapterRegistry and the delete guard. */
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Promoted-plugin migration - one-shot retirement of plugins that became core.
3
+ *
4
+ * A plugin that is promoted into the CMS leaves two traces on an existing
5
+ * install:
6
+ *
7
+ * 1. its `plugins.json` entry, which would otherwise offer a toggle for a
8
+ * feature that is no longer optional, and
9
+ * 2. `plugins/<slug>/` itself - the updater replaces the plugins it ships and
10
+ * adds new ones, but never removes one that has gone away. Left in place,
11
+ * the loader would register a SECOND copy of a feature the CMS now has of
12
+ * its own, which for a shortcode means two handlers racing for the same
13
+ * tag.
14
+ *
15
+ * This is the generic form of `search-migration.js`, which did the same job for
16
+ * `site-search` alone. Each promotion adds a row to PROMOTED rather than another
17
+ * near-identical file.
18
+ *
19
+ * Idempotent: with no entry and no directory there is nothing to do, which is
20
+ * the state of every fresh install.
21
+ */
22
+ import fs from 'fs';
23
+ import path from 'path';
24
+
25
+ /**
26
+ * Plugins that are now part of the CMS.
27
+ *
28
+ * `author` is checked against the directory's manifest before anything is
29
+ * deleted. A directory of that name carrying somebody else's work is their
30
+ * plugin, not our retired one, and is left exactly where it is.
31
+ *
32
+ * `defaults` is what the plugin's own config.js shipped. Settings matching it
33
+ * are dropped silently; anything else is reported, because a site that tuned a
34
+ * setting deserves to be told where it went rather than have it vanish.
35
+ *
36
+ * `lift(settings, enabled)` moves those settings somewhere they still matter.
37
+ * A row without one is a plugin whose settings core does not need. It runs
38
+ * BEFORE the entry is deleted, so a throw leaves the entry in place and the
39
+ * next start tries again rather than losing the values.
40
+ */
41
+ const PROMOTED = [
42
+ {
43
+ slug: 'demo-viewer',
44
+ author: 'Domma',
45
+ defaults: {basePath: '/public/demos', defaultHeight: '500px'},
46
+ note: 'the [demo] shortcode is built in'
47
+ },
48
+ {
49
+ slug: 'notes',
50
+ author: 'Darryl Waterhouse',
51
+ defaults: {scope: 'user', maxPerUser: 1000, storage: {adapter: 'mongodb', connection: 'default'}},
52
+ note: 'Notes is a built-in Tool; its entries are untouched in notes-notes'
53
+ },
54
+ {
55
+ slug: 'todo',
56
+ author: 'Darryl Waterhouse',
57
+ defaults: {scope: 'user', defaultPriority: 'medium', maxPerUser: 500, storage: {adapter: 'mongodb', connection: 'default'}},
58
+ note: 'Todo is a built-in Tool; its entries are untouched in todo-items'
59
+ },
60
+ {
61
+ slug: 'contacts',
62
+ author: 'Darryl Waterhouse',
63
+ defaults: {scope: 'user'},
64
+ note: 'Contacts is a built-in Tool; contacts-contacts and contacts-groups are untouched'
65
+ },
66
+ {
67
+ slug: 'analytics',
68
+ author: 'Darryl Waterhouse',
69
+ note: 'analytics is built in; counters moved to content/analytics/',
70
+ lift: liftAnalyticsCounters
71
+ },
72
+ {
73
+ slug: 'theme-switcher',
74
+ author: 'Domma',
75
+ note: 'the switcher is a theme setting now',
76
+ lift: liftThemeSwitcher
77
+ }
78
+ ];
79
+
80
+ /**
81
+ * Move the analytics counters out from under plugins/.
82
+ *
83
+ * This is the only lift that moves data a site cannot regenerate: lifetime.json,
84
+ * daily.json and journeys.json are real traffic history, tens to hundreds of
85
+ * kilobytes per site. The directory they sit in is about to be deleted, so the
86
+ * copy has to happen first and has to be verified.
87
+ *
88
+ * Copy, re-read, compare, and only then let the caller proceed. A mismatch
89
+ * throws, which abandons the whole row - the plugin directory stays, the entry
90
+ * stays, and the next start tries again. A site keeping a duplicate 96K for one
91
+ * release is not a problem; losing the original is.
92
+ *
93
+ * Files already present at the destination are left alone: a second run must not
94
+ * overwrite counters that have moved on since.
95
+ *
96
+ * @returns {string|null} what changed, for the log
97
+ */
98
+ function liftAnalyticsCounters() {
99
+ const SRC = path.resolve('plugins', 'analytics', 'data');
100
+ const DEST = path.resolve('content', 'analytics');
101
+ if (!fs.existsSync(SRC)) return null;
102
+
103
+ fs.mkdirSync(DEST, {recursive: true});
104
+ const copied = [];
105
+
106
+ for (const name of ['lifetime.json', 'daily.json', 'journeys.json', 'stats.json']) {
107
+ const from = path.join(SRC, name);
108
+ const to = path.join(DEST, name);
109
+ if (!fs.existsSync(from) || fs.existsSync(to)) continue;
110
+
111
+ const raw = fs.readFileSync(from);
112
+ fs.writeFileSync(to, raw);
113
+
114
+ // Verified by comparing what is now on disk, not by trusting the write.
115
+ const back = fs.readFileSync(to);
116
+ if (!back.equals(raw)) {
117
+ throw new Error(`counters failed to copy intact (${name})`);
118
+ }
119
+ copied.push(`${name} (${raw.length} bytes)`);
120
+ }
121
+
122
+ return copied.length
123
+ ? `copied counters to content/analytics/: ${copied.join(', ')}`
124
+ : null;
125
+ }
126
+
127
+ /**
128
+ * Move the theme switcher's placement into config/theme.json.
129
+ *
130
+ * `enabled` carries across deliberately: a site showing the disc keeps showing
131
+ * it, and one that never turned the plugin on does not suddenly acquire a
132
+ * visitor-facing theme picker.
133
+ *
134
+ * theme.json is read and written directly rather than through saveThemeConfig(),
135
+ * which validates and recompiles. Migrations run before the app is up, and a
136
+ * placement value is not worth pulling that machinery into startup. Written only
137
+ * when theme.json already exists - migrateThemeConfig() runs first and creates
138
+ * it, so an absent file means something is wrong enough not to paper over.
139
+ *
140
+ * @param {object} settings
141
+ * @param {boolean} enabled
142
+ * @returns {string|null} what changed, for the log
143
+ */
144
+ function liftThemeSwitcher(settings, enabled) {
145
+ const THEME_CONFIG = path.resolve('config', 'theme.json');
146
+ if (!fs.existsSync(THEME_CONFIG)) return null;
147
+
148
+ let cfg;
149
+ try {
150
+ cfg = JSON.parse(fs.readFileSync(THEME_CONFIG, 'utf8'));
151
+ } catch {
152
+ return null;
153
+ }
154
+ if (cfg.switcher && cfg.switcher.enabled !== undefined) {
155
+ return null; // already lifted, or configured since
156
+ }
157
+
158
+ const POSITIONS = ['right-center', 'left-center', 'bottom-right', 'bottom-left',
159
+ 'top-left', 'top-right'];
160
+ const num = (v, fallback) =>
161
+ (Number.isFinite(Number(v)) && Number(v) >= 0 && Number(v) <= 400) ? Number(v) : fallback;
162
+
163
+ cfg.switcher = {
164
+ enabled,
165
+ position: POSITIONS.includes(settings.position) ? settings.position : 'right-center',
166
+ offsetX: num(settings.offsetX, 32),
167
+ offsetY: num(settings.offsetY, 32),
168
+ // The plugin's `themes` list was never read by its own markup, which
169
+ // hardcoded 26 dots. Starting empty means "every theme", which is what
170
+ // the markup effectively promised and never delivered.
171
+ themes: []
172
+ };
173
+ fs.writeFileSync(THEME_CONFIG, JSON.stringify(cfg, null, 2) + '\n');
174
+ return `lifted placement into theme.json (enabled: ${enabled})`;
175
+ }
176
+
177
+ /**
178
+ * Run the migration.
179
+ *
180
+ * Paths are resolved per call rather than at import, so the same process can run
181
+ * this against more than one install - which the tests do.
182
+ *
183
+ * @param {object[]} [table] - override for testing
184
+ * @returns {string[]} what changed, for the startup log (empty = nothing to do)
185
+ */
186
+ export function migratePromotedPlugins(table = PROMOTED) {
187
+ const PLUGINS_CONFIG = path.resolve('config', 'plugins.json');
188
+ const changes = [];
189
+
190
+ let plugins = null;
191
+ try {
192
+ plugins = JSON.parse(fs.readFileSync(PLUGINS_CONFIG, 'utf8'));
193
+ } catch {
194
+ // No plugins.json, or a malformed one. Neither is this migration's
195
+ // business - a broken file is reported loudly elsewhere.
196
+ }
197
+
198
+ let dirty = false;
199
+
200
+ for (const {slug, author, defaults = {}, note, lift} of table) {
201
+ const stored = plugins?.[slug];
202
+
203
+ // 1. The entry, and any settings it carried.
204
+ if (plugins && stored) {
205
+ const settings = stored.settings || {};
206
+
207
+ if (lift) {
208
+ // A lift that throws must abandon THIS row entirely - entry and
209
+ // directory both. Deleting either after a failed lift is how a
210
+ // site loses the thing the lift was moving.
211
+ let lifted;
212
+ try {
213
+ lifted = lift(settings, stored.enabled === true);
214
+ } catch (err) {
215
+ changes.push(`${slug}: NOT retired - ${err.message}`);
216
+ continue;
217
+ }
218
+ if (lifted) changes.push(`${slug}: ${lifted}`);
219
+ } else {
220
+ const changed = Object.entries(settings)
221
+ .filter(([k, v]) => JSON.stringify(v) !== JSON.stringify(defaults[k]));
222
+ if (changed.length) {
223
+ changes.push(
224
+ `${slug}: dropped non-default settings (${changed.map(([k]) => k).join(', ')}) - ${note}`
225
+ );
226
+ }
227
+ }
228
+
229
+ delete plugins[slug];
230
+ dirty = true;
231
+ changes.push(`${slug}: removed the plugins.json entry`);
232
+ }
233
+
234
+ // 2. The directory - but only if it is the one we shipped.
235
+ const dir = path.resolve('plugins', slug);
236
+ if (!fs.existsSync(dir)) continue;
237
+
238
+ let ours = false;
239
+ try {
240
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'plugin.json'), 'utf8'));
241
+ ours = manifest.name === slug && manifest.author === author;
242
+ } catch { /* no readable manifest - leave it alone */ }
243
+
244
+ if (ours) {
245
+ fs.rmSync(dir, {recursive: true, force: true});
246
+ changes.push(`${slug}: removed plugins/${slug}/`);
247
+ }
248
+ }
249
+
250
+ if (dirty) {
251
+ fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(plugins, null, 2) + '\n');
252
+ }
253
+ return changes;
254
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Public asset CORS
3
+ *
4
+ * A module script is fetched in CORS mode, so `import … from
5
+ * 'https://this-site/public/whatever/index.js'` on another origin fails without
6
+ * Access-Control-Allow-Origin. A site that deliberately wants some of its own
7
+ * assets hot-linkable - engine source, demo code, a shared bundle - needs that
8
+ * header on those paths and nowhere else.
9
+ *
10
+ * `@fastify/cors` is registered once with `config/server.json`'s `cors` block and
11
+ * decides per ORIGIN, not per path, so it cannot express "open reads under this
12
+ * one prefix". Hence `cors.publicPrefixes`.
13
+ *
14
+ * Reads only. This never widens a write, and it never applies to a path that was
15
+ * not explicitly listed.
16
+ */
17
+
18
+ /**
19
+ * Read and sanitise the configured prefixes.
20
+ *
21
+ * A prefix must be an absolute path. Anything else is dropped rather than
22
+ * corrected: a typo that silently matched everything would be the one mistake
23
+ * here with real consequences.
24
+ *
25
+ * @param {object} [corsConfig] - the `cors` block from config/server.json
26
+ * @returns {string[]}
27
+ */
28
+ export function normalisePublicPrefixes(corsConfig) {
29
+ const raw = corsConfig?.publicPrefixes;
30
+ if (!Array.isArray(raw)) return [];
31
+ return raw.filter(p => typeof p === 'string' && p.startsWith('/') && p.length > 1);
32
+ }
33
+
34
+ /**
35
+ * Does this request qualify for the open-read header?
36
+ *
37
+ * @param {string} method
38
+ * @param {string} url - request URL, path first (query is ignored by startsWith)
39
+ * @param {string[]} prefixes - from normalisePublicPrefixes()
40
+ * @returns {boolean}
41
+ */
42
+ export function isPublicAssetRead(method, url, prefixes) {
43
+ if (method !== 'GET' && method !== 'HEAD') return false;
44
+ if (!prefixes.length || typeof url !== 'string') return false;
45
+ return prefixes.some(p => url.startsWith(p));
46
+ }