domma-cms 0.44.0 → 0.45.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.
Files changed (254) hide show
  1. package/CLAUDE.md +53 -53
  2. package/README.md +27 -27
  3. package/admin/css/admin.css +1 -1
  4. package/admin/dist/domma/domma-tools.css +3 -3
  5. package/admin/dist/domma/domma-tools.min.js +3 -3
  6. package/admin/index.html +2 -2
  7. package/admin/js/app.js +1 -1
  8. package/admin/js/lib/card-builder.js +2 -2
  9. package/admin/js/lib/crud-tutorial.js +1 -1
  10. package/admin/js/lib/effect-defs.js +1 -1
  11. package/admin/js/lib/effects-builder.js +1 -1
  12. package/admin/js/lib/image-editor.js +1 -1
  13. package/admin/js/lib/markdown-toolbar.js +2 -2
  14. package/admin/js/lib/project-context.js +1 -1
  15. package/admin/js/lib/project-quick-create.js +1 -1
  16. package/admin/js/lib/scribe-composer.js +1 -1
  17. package/admin/js/lib/server-restart.js +1 -1
  18. package/admin/js/lib/shortcode-context-menu.js +2 -2
  19. package/admin/js/lib/simple-editor.js +1 -1
  20. package/admin/js/lib/themes.js +1 -1
  21. package/admin/js/lib/timeline-builder.js +1 -1
  22. package/admin/js/templates/action-editor.html +8 -8
  23. package/admin/js/templates/api-endpoint-editor.html +7 -7
  24. package/admin/js/templates/api-endpoints.html +1 -1
  25. package/admin/js/templates/api-reference.html +1 -1
  26. package/admin/js/templates/block-editor.html +6 -6
  27. package/admin/js/templates/collection-editor.html +1 -1
  28. package/admin/js/templates/component-editor.html +2 -2
  29. package/admin/js/templates/dashboard/cache.html +3 -3
  30. package/admin/js/templates/dashboard/journeys.html +3 -3
  31. package/admin/js/templates/dashboard/kpi-strip.html +3 -3
  32. package/admin/js/templates/dashboard/top-pages.html +1 -1
  33. package/admin/js/templates/dashboard/traffic-chart.html +1 -1
  34. package/admin/js/templates/docs/api-actions.html +1 -1
  35. package/admin/js/templates/docs/api-collections.html +4 -4
  36. package/admin/js/templates/docs/api-pages.html +2 -2
  37. package/admin/js/templates/docs/api-plugins.html +1 -1
  38. package/admin/js/templates/docs/api-settings.html +2 -2
  39. package/admin/js/templates/docs/api-users.html +1 -1
  40. package/admin/js/templates/docs/components-howto.html +4 -4
  41. package/admin/js/templates/docs/components-reference.html +21 -21
  42. package/admin/js/templates/docs/components-rules.html +14 -14
  43. package/admin/js/templates/docs/components-walkthrough.html +13 -13
  44. package/admin/js/templates/docs/tutorial-crud.html +49 -49
  45. package/admin/js/templates/docs/tutorial-forms.html +4 -4
  46. package/admin/js/templates/docs/tutorial-plugin.html +10 -10
  47. package/admin/js/templates/docs/usage-actions.html +6 -6
  48. package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
  49. package/admin/js/templates/docs/usage-dconfig.html +4 -4
  50. package/admin/js/templates/docs/usage-navigation.html +1 -1
  51. package/admin/js/templates/docs/usage-pages.html +1 -1
  52. package/admin/js/templates/docs/usage-shortcodes.html +24 -24
  53. package/admin/js/templates/docs/usage-users-roles.html +3 -3
  54. package/admin/js/templates/docs/usage-views.html +4 -4
  55. package/admin/js/templates/documentation.html +1 -1
  56. package/admin/js/templates/effects.html +752 -752
  57. package/admin/js/templates/form-editor.html +5 -5
  58. package/admin/js/templates/forms.html +17 -17
  59. package/admin/js/templates/login.html +5 -5
  60. package/admin/js/templates/menu-editor.html +10 -10
  61. package/admin/js/templates/my-profile.html +17 -17
  62. package/admin/js/templates/navigation.html +11 -11
  63. package/admin/js/templates/notifications.html +1 -1
  64. package/admin/js/templates/page-editor.html +4 -4
  65. package/admin/js/templates/pro-docs.html +14 -14
  66. package/admin/js/templates/role-editor.html +70 -70
  67. package/admin/js/templates/roles.html +10 -10
  68. package/admin/js/templates/settings.html +10 -10
  69. package/admin/js/templates/tutorials.html +3 -3
  70. package/admin/js/templates/view-editor.html +15 -15
  71. package/admin/js/views/action-editor.js +1 -1
  72. package/admin/js/views/actions-list.js +1 -1
  73. package/admin/js/views/api-endpoint-editor.js +2 -2
  74. package/admin/js/views/api-endpoints.js +2 -2
  75. package/admin/js/views/api-tokens.js +3 -3
  76. package/admin/js/views/blocks.js +2 -2
  77. package/admin/js/views/collection-editor.js +1 -1
  78. package/admin/js/views/collection-entries.js +1 -1
  79. package/admin/js/views/collections.js +1 -1
  80. package/admin/js/views/component-editor.js +1 -1
  81. package/admin/js/views/components.js +4 -4
  82. package/admin/js/views/dashboard/widgets/cache.js +1 -1
  83. package/admin/js/views/dashboard/widgets/journeys.js +1 -1
  84. package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
  85. package/admin/js/views/form-editor.js +7 -7
  86. package/admin/js/views/form-submissions.js +1 -1
  87. package/admin/js/views/forms.js +1 -1
  88. package/admin/js/views/layouts.js +1 -1
  89. package/admin/js/views/login.js +2 -2
  90. package/admin/js/views/menu-editor.js +3 -3
  91. package/admin/js/views/menu-locations.js +1 -1
  92. package/admin/js/views/menus.js +3 -3
  93. package/admin/js/views/navigation.js +8 -8
  94. package/admin/js/views/page-editor.js +40 -40
  95. package/admin/js/views/pages.js +3 -3
  96. package/admin/js/views/plugin-code.js +2 -2
  97. package/admin/js/views/plugins.js +1 -1
  98. package/admin/js/views/project-settings.js +1 -1
  99. package/admin/js/views/projects.js +1 -1
  100. package/admin/js/views/roles.js +1 -1
  101. package/admin/js/views/user-editor.js +1 -1
  102. package/admin/js/views/users.js +3 -3
  103. package/admin/js/views/view-editor.js +1 -1
  104. package/admin/js/views/view-preview.js +1 -1
  105. package/admin/js/views/views-list.js +1 -1
  106. package/bin/cli.js +8 -8
  107. package/bin/lib/config-merge.js +44 -44
  108. package/bin/update.js +16 -16
  109. package/config/plugins.json +1 -1
  110. package/package.json +2 -2
  111. package/plugins/_template/admin/views/index.js +1 -1
  112. package/plugins/_template/config.js +1 -1
  113. package/plugins/_template/plugin.js +1 -1
  114. package/plugins/analytics/admin/templates/analytics.html +3 -3
  115. package/plugins/analytics/admin/views/analytics.js +1 -1
  116. package/plugins/analytics/config.js +1 -1
  117. package/plugins/analytics/plugin.js +4 -4
  118. package/plugins/analytics/plugin.json +1 -1
  119. package/plugins/analytics/public/inject-body.html +2 -2
  120. package/plugins/analytics/public/inject-head.html +1 -1
  121. package/plugins/blog/admin/templates/blog.html +4 -4
  122. package/plugins/blog/admin/views/blog.js +3 -3
  123. package/plugins/blog/admin/views/categories.js +2 -2
  124. package/plugins/blog/admin/views/comments.js +4 -4
  125. package/plugins/blog/admin/views/post-editor.js +1 -1
  126. package/plugins/blog/plugin.js +2 -2
  127. package/plugins/blog/plugin.json +4 -4
  128. package/plugins/contacts/admin/views/contacts.js +8 -8
  129. package/plugins/contacts/plugin.js +4 -4
  130. package/plugins/contacts/plugin.json +1 -1
  131. package/plugins/invoice/admin/templates/editor.html +6 -6
  132. package/plugins/invoice/admin/templates/index.html +4 -4
  133. package/plugins/invoice/admin/views/editor.js +3 -3
  134. package/plugins/invoice/admin/views/index.js +4 -4
  135. package/plugins/invoice/admin/views/issuers.js +1 -1
  136. package/plugins/invoice/admin/views/party-view.js +3 -3
  137. package/plugins/invoice/admin/views/receivers.js +1 -1
  138. package/plugins/invoice/config.js +1 -1
  139. package/plugins/invoice/plugin.js +1 -1
  140. package/plugins/invoice/plugin.json +4 -4
  141. package/plugins/notes/admin/views/notes.js +4 -4
  142. package/plugins/notes/plugin.js +1 -1
  143. package/plugins/notes/plugin.json +1 -1
  144. package/plugins/site-search/admin/templates/site-search.html +3 -3
  145. package/plugins/site-search/admin/views/site-search.js +1 -1
  146. package/plugins/site-search/config.js +1 -1
  147. package/plugins/site-search/plugin.js +4 -4
  148. package/plugins/site-search/plugin.json +1 -1
  149. package/plugins/surveys/admin/templates/results.html +1 -1
  150. package/plugins/surveys/admin/templates/survey-editor.html +4 -4
  151. package/plugins/surveys/admin/views/audience.js +6 -6
  152. package/plugins/surveys/admin/views/results.js +3 -3
  153. package/plugins/surveys/admin/views/survey-editor.js +1 -1
  154. package/plugins/surveys/admin/views/surveys.js +2 -2
  155. package/plugins/surveys/plugin.js +1 -1
  156. package/plugins/surveys/plugin.json +4 -4
  157. package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
  158. package/plugins/theme-switcher/plugin.json +3 -3
  159. package/plugins/theme-switcher/public/inject-body.html +28 -28
  160. package/plugins/theme-switcher/public/inject-head.html +1 -1
  161. package/plugins/todo/plugin.js +1 -1
  162. package/public/css/forms.css +1 -1
  163. package/public/js/collection-browser.js +1 -1
  164. package/public/js/collection-context.js +1 -1
  165. package/public/js/form-logic-engine.js +1 -1
  166. package/public/js/forms.js +2 -1
  167. package/public/js/site.js +1 -1
  168. package/scripts/build.js +14 -14
  169. package/scripts/create-plugin.js +2 -2
  170. package/scripts/fresh.js +2 -2
  171. package/scripts/gen-instance-secret.js +1 -1
  172. package/scripts/pro.js +12 -12
  173. package/scripts/reset.js +2 -2
  174. package/scripts/setup.js +15 -15
  175. package/scripts/users.js +5 -5
  176. package/scripts/verify-assets.mjs +6 -6
  177. package/server/config.js +1 -1
  178. package/server/middleware/auth.js +253 -253
  179. package/server/middleware/managerAuth.js +3 -3
  180. package/server/routes/api/actions.js +4 -4
  181. package/server/routes/api/api-endpoints.js +7 -7
  182. package/server/routes/api/api-tokens.js +5 -5
  183. package/server/routes/api/auth.js +309 -309
  184. package/server/routes/api/blocks.js +3 -3
  185. package/server/routes/api/collections.js +18 -18
  186. package/server/routes/api/dashboard.js +5 -5
  187. package/server/routes/api/effects.js +2 -2
  188. package/server/routes/api/endpoints-public.js +5 -5
  189. package/server/routes/api/forms.js +47 -47
  190. package/server/routes/api/menu-locations.js +3 -3
  191. package/server/routes/api/menus.js +7 -7
  192. package/server/routes/api/navigation.js +42 -42
  193. package/server/routes/api/notifications.js +2 -2
  194. package/server/routes/api/pages.js +14 -2
  195. package/server/routes/api/plugin-marketplace.js +3 -3
  196. package/server/routes/api/plugins.js +6 -6
  197. package/server/routes/api/projects.js +9 -9
  198. package/server/routes/api/scaffold.js +7 -7
  199. package/server/routes/api/settings.js +6 -6
  200. package/server/routes/api/sidebar.js +1 -1
  201. package/server/routes/api/users.js +5 -5
  202. package/server/routes/api/versions.js +1 -1
  203. package/server/routes/api/views.js +1 -1
  204. package/server/routes/docs-public.js +3 -3
  205. package/server/routes/public.js +14 -14
  206. package/server/server.js +34 -16
  207. package/server/services/actions.js +19 -19
  208. package/server/services/adapters/FileAdapter.js +2 -2
  209. package/server/services/adapters/MongoAdapter.js +5 -5
  210. package/server/services/apiEndpoints.js +15 -15
  211. package/server/services/apiTokens.js +7 -7
  212. package/server/services/blocks.js +13 -13
  213. package/server/services/cache/drivers/MemoryDriver.js +3 -3
  214. package/server/services/cache/drivers/NoneDriver.js +1 -1
  215. package/server/services/cache/index.js +5 -5
  216. package/server/services/collections.js +14 -14
  217. package/server/services/components.js +9 -9
  218. package/server/services/connectionManager.js +4 -4
  219. package/server/services/content.js +25 -2
  220. package/server/services/docs.js +14 -14
  221. package/server/services/email.js +167 -167
  222. package/server/services/filterEngine.js +6 -6
  223. package/server/services/forms.js +18 -10
  224. package/server/services/health.js +1 -1
  225. package/server/services/hooks.js +7 -7
  226. package/server/services/images.js +4 -4
  227. package/server/services/managerClient.js +1 -1
  228. package/server/services/markdown.js +79 -79
  229. package/server/services/menuRender.js +5 -5
  230. package/server/services/menus-migration.js +3 -3
  231. package/server/services/menus.js +31 -31
  232. package/server/services/permissionRegistry.js +1 -1
  233. package/server/services/pluginFiles.js +5 -5
  234. package/server/services/pluginInstaller.js +17 -17
  235. package/server/services/pluginScaffold.js +1 -1
  236. package/server/services/plugins.js +17 -17
  237. package/server/services/presetCollections.js +2 -2
  238. package/server/services/projects.js +35 -26
  239. package/server/services/recipes/contact-list.json +1 -1
  240. package/server/services/recipes/onboarding.json +8 -8
  241. package/server/services/references.js +8 -8
  242. package/server/services/renderer.js +9 -9
  243. package/server/services/roles.js +9 -9
  244. package/server/services/rowAccess.js +10 -10
  245. package/server/services/scaffolder.js +30 -30
  246. package/server/services/sidebar-migration.js +10 -10
  247. package/server/services/sitemap.js +1 -1
  248. package/server/services/userProfiles.js +199 -199
  249. package/server/services/userRoles.js +7 -7
  250. package/server/services/users.js +302 -302
  251. package/server/services/versions.js +3 -3
  252. package/server/services/viewPipeline.js +6 -6
  253. package/server/services/views.js +11 -11
  254. package/server/templates/page.html +5 -5
@@ -31,7 +31,7 @@
31
31
  * DELETE /v1/:slug/:id ≡ DELETE /collections/:slug/public/:id
32
32
  *
33
33
  * Access modes per verb (`schema.api.<verb>.access`): 'public', a role name
34
- * (JWT + role level), or 'token' (project-scoped API token — see
34
+ * (JWT + role level), or 'token' (project-scoped API token - see
35
35
  * services/apiTokens.js). A token is ONLY accepted when the mode is 'token';
36
36
  * it is never a substitute for a role, and a JWT is never accepted in token
37
37
  * mode. `schema.api.read.fields` optionally whitelists which data fields the
@@ -116,7 +116,7 @@ function redactTokenHash(entry) {
116
116
  /**
117
117
  * Apply the read-field allowlist (`schema.api.read.fields`) to a public read
118
118
  * payload. Absent or empty allowlist = all fields. Handles both list payloads
119
- * ({entries: [...]}) and single entries. Only entry.data is filtered —
119
+ * ({entries: [...]}) and single entries. Only entry.data is filtered -
120
120
  * `_refs` from resolveRefs is not (refs of stripped fields may still appear;
121
121
  * documented v1 behaviour).
122
122
  *
@@ -147,10 +147,10 @@ export function applyReadFieldAllowlist(schema, payload) {
147
147
  */
148
148
  export async function checkPublicAccess(schema, operation, request, reply) {
149
149
  // Hard kill-switch: if the artefact's project is disabled, the whole
150
- // external surface is off — 404 (no oracle). This covers /api/v1
150
+ // external surface is off - 404 (no oracle). This covers /api/v1
151
151
  // collections, the synthesized-schema custom endpoints (/api/x/*, whose
152
152
  // meta.project is the endpoint's project), and any token bound to the
153
- // project — all funnel through here.
153
+ // project - all funnel through here.
154
154
  if (!(await isProjectEnabled(resolveArtefactProject(schema)))) {
155
155
  return reply.status(404).send({ error: 'Not found' });
156
156
  }
@@ -162,7 +162,7 @@ export async function checkPublicAccess(schema, operation, request, reply) {
162
162
 
163
163
  if (access.access === 'public') return; // No auth needed
164
164
 
165
- // Token mode — project-scoped API token, strict: a token is the ONLY
165
+ // Token mode - project-scoped API token, strict: a token is the ONLY
166
166
  // accepted credential here (a JWT never satisfies token mode, and a
167
167
  // token never satisfies a role mode).
168
168
  if (access.access === 'token') {
@@ -184,7 +184,7 @@ export async function checkPublicAccess(schema, operation, request, reply) {
184
184
  return;
185
185
  }
186
186
 
187
- // Auth required — try to verify JWT
187
+ // Auth required - try to verify JWT
188
188
  try {
189
189
  await request.jwtVerify();
190
190
  } catch {
@@ -193,7 +193,7 @@ export async function checkPublicAccess(schema, operation, request, reply) {
193
193
 
194
194
  const user = request.user;
195
195
  const requiredLevel = roleLevel(access.access);
196
- // Effective level — multi-role users get the lowest (most privileged) level
196
+ // Effective level - multi-role users get the lowest (most privileged) level
197
197
  // across their primary + additional roles for this access decision.
198
198
  const userLevel = getEffectiveLevel(user);
199
199
 
@@ -307,9 +307,9 @@ export async function collectionsRoutes(fastify) {
307
307
  * GET /api/collections/:slug/entries
308
308
  *
309
309
  * Query parameters:
310
- * page, limit, sort, order — pagination/sort (limit=0 means "no limit")
311
- * search — free-text substring across all field values
312
- * filter[<field>_<op>]=val — structured filter; see filterEngine.js
310
+ * page, limit, sort, order - pagination/sort (limit=0 means "no limit")
311
+ * search - free-text substring across all field values
312
+ * filter[<field>_<op>]=val - structured filter; see filterEngine.js
313
313
  *
314
314
  * Example:
315
315
  * /api/collections/jobs/entries?filter[location]=London&filter[salary_gte]=50000
@@ -388,7 +388,7 @@ export async function collectionsRoutes(fastify) {
388
388
  }
389
389
  });
390
390
 
391
- // Clear all entries — DELETE /collections/:slug/entries (no :id)
391
+ // Clear all entries - DELETE /collections/:slug/entries (no :id)
392
392
  fastify.delete('/collections/:slug/entries', canDelete, async (request, reply) => {
393
393
  try {
394
394
  await clearEntries(request.params.slug);
@@ -493,11 +493,11 @@ export async function collectionsRoutes(fastify) {
493
493
  /*
494
494
  * GET /api/collections/:slug/public
495
495
  *
496
- * Public read endpoint — gated by `api.read` in the collection schema.
496
+ * Public read endpoint - gated by `api.read` in the collection schema.
497
497
  * Supports the same pagination/sort/search/filter as the admin endpoint
498
498
  * (see GET /collections/:slug/entries above), plus:
499
499
  *
500
- * scope=mine — restrict results to entries created by the current
500
+ * scope=mine - restrict results to entries created by the current
501
501
  * user (`meta.createdBy === user.id`). Requires a valid
502
502
  * JWT; returns 401 if unauthenticated. Used by the
503
503
  * `[collection scope="mine"]` shortcode for per-user
@@ -566,11 +566,11 @@ export async function collectionsRoutes(fastify) {
566
566
  * resolved here and the rendered HTML fragment swapped in client-side.
567
567
  *
568
568
  * Body: { attrs: <base64 JSON of the original shortcode attributes> }
569
- * Auth: JWT required — `createdBy = <user.id>` is injected server-side
569
+ * Auth: JWT required - `createdBy = <user.id>` is injected server-side
570
570
  * (the client cannot tamper with which user's data they see).
571
571
  * Returns: { html: '<div class="dm-collection-…">…</div>' }
572
572
  *
573
- * Security: results are always scoped to `createdBy = <user.id>` —
573
+ * Security: results are always scoped to `createdBy = <user.id>` -
574
574
  * a caller can only ever render their own entries, regardless of the
575
575
  * collection's `api.read` setting. This matches the security model
576
576
  * of GET /:slug/public?scope=mine.
@@ -586,11 +586,11 @@ export async function collectionsRoutes(fastify) {
586
586
  *
587
587
  * Body: {
588
588
  * attrs: <base64 JSON of the shortcode attributes>,
589
- * page: <number, optional — applied via attrs.limit + slicing>
589
+ * page: <number, optional - applied via attrs.limit + slicing>
590
590
  * }
591
591
  *
592
592
  * Auth: gated by the target collection's `api.read` setting via
593
- * checkPublicAccess(). No user identity is injected — this
593
+ * checkPublicAccess(). No user identity is injected - this
594
594
  * endpoint is for general fragment rendering, not per-user
595
595
  * scoping (that's render-scope's job).
596
596
  *
@@ -712,7 +712,7 @@ export async function collectionsRoutes(fastify) {
712
712
  fastify.delete('/collections/:slug/public/:id', publicDeleteEntry);
713
713
 
714
714
  // -------------------------------------------------------------------------
715
- // External versioned API — stable alias of the public endpoints above.
715
+ // External versioned API - stable alias of the public endpoints above.
716
716
  // Documented surface for external consumers (docs/api-reference.md).
717
717
  // -------------------------------------------------------------------------
718
718
 
@@ -98,7 +98,7 @@ function buildTopPages(daily, days = 7) {
98
98
  .slice(0, 5);
99
99
  }
100
100
 
101
- // ── Journeys / spikes — ported verbatim from plugins/analytics so the core
101
+ // ── Journeys / spikes - ported verbatim from plugins/analytics so the core
102
102
  // dashboard can compute them off disk without the plugin's encapsulated
103
103
  // `fastify.analytics` decorator (which it cannot see). Keep in sync with
104
104
  // plugins/analytics/plugin.js.
@@ -181,7 +181,7 @@ function detectSpikes(daily, now = new Date()) {
181
181
  }
182
182
 
183
183
  /**
184
- * Recent activity feed — combines page version events with recent collection
184
+ * Recent activity feed - combines page version events with recent collection
185
185
  * entries (form submissions). Returns at most 10 items, newest first.
186
186
  *
187
187
  * @returns {Promise<Array<object>>}
@@ -189,7 +189,7 @@ function detectSpikes(daily, now = new Date()) {
189
189
  async function buildActivity() {
190
190
  const items = [];
191
191
 
192
- // Versions — read each page's _meta.json via the service so the schema
192
+ // Versions - read each page's _meta.json via the service so the schema
193
193
  // stays in one place. listVersions returns newest-first.
194
194
  const versionsDir = path.join(ROOT, 'content', 'versions');
195
195
  try {
@@ -211,7 +211,7 @@ async function buildActivity() {
211
211
  }
212
212
  } catch { /* no versions dir */ }
213
213
 
214
- // Collection entries — each slug has a single data.json with an array of
214
+ // Collection entries - each slug has a single data.json with an array of
215
215
  // entries. Take the newest ENTRIES_PER_COLLECTION by meta.createdAt.
216
216
  const collectionsDir = path.join(ROOT, 'content', 'collections');
217
217
  try {
@@ -277,7 +277,7 @@ export async function dashboardRoutes(fastify, opts = {}) {
277
277
  const analytics = fastify.analytics;
278
278
  // The analytics plugin decorates `analytics` inside its own prefixed,
279
279
  // encapsulated scope, so `fastify.analytics` is undefined here in the
280
- // core dashboard route — traffic/topPages always read 0 even though hits
280
+ // core dashboard route - traffic/topPages always read 0 even though hits
281
281
  // are recorded. Read the plugin's daily counters straight off disk (same
282
282
  // pattern as buildActivity reading versions/collections below).
283
283
  const daily = (await safe('analytics.daily', async () => {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Effects API Routes
3
- * GET /api/effects/settings — public (read by public site IIFE)
4
- * PUT /api/effects/settings — admin-only (save user overrides)
3
+ * GET /api/effects/settings - public (read by public site IIFE)
4
+ * PUT /api/effects/settings - admin-only (save user overrides)
5
5
  */
6
6
  import {authenticate, requireAdmin} from '../../middleware/auth.js';
7
7
  import {getConfig, saveConfig} from '../../config.js';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Custom API Endpoints — public surface
2
+ * Custom API Endpoints - public surface
3
3
  *
4
4
  * One catch-all serves every user-defined endpoint:
5
5
  * GET /api/x/<project><path> e.g. /api/x/world-cup/fixtures-day/2026-06-11
@@ -7,7 +7,7 @@
7
7
  * Definitions live in the `api-endpoints` preset collection and are matched
8
8
  * at request time by services/apiEndpoints.js. Auth (public / token / role),
9
9
  * project binding, token scopes, and the read field allowlist all reuse the
10
- * public collections machinery via a synthesized schema — one battle-tested
10
+ * public collections machinery via a synthesized schema - one battle-tested
11
11
  * code path for every external read.
12
12
  *
13
13
  * Unknown project, unknown path, and disabled endpoints all 404 identically
@@ -17,7 +17,7 @@ import {EndpointParamError, executeEndpoint, matchEndpoint} from '../../services
17
17
  import {applyReadFieldAllowlist, checkPublicAccess} from './collections.js';
18
18
  import * as cache from '../../services/cache/index.js';
19
19
 
20
- /** Cached sentinel for single-mode misses — a miss is a cacheable answer. */
20
+ /** Cached sentinel for single-mode misses - a miss is a cacheable answer. */
21
21
  const NOT_FOUND = {__endpointNotFound: true};
22
22
 
23
23
  /**
@@ -44,7 +44,7 @@ export async function endpointsPublicRoutes(fastify) {
44
44
 
45
45
  const {def, params} = match;
46
46
 
47
- // Synthesized schema — checkPublicAccess reads exactly api.read,
47
+ // Synthesized schema - checkPublicAccess reads exactly api.read,
48
48
  // slug (token scopes), and meta.project (token project binding).
49
49
  const synth = {
50
50
  slug: def.collection,
@@ -63,7 +63,7 @@ export async function endpointsPublicRoutes(fastify) {
63
63
  };
64
64
 
65
65
  try {
66
- // Only public responses are cacheable — token/role responses are
66
+ // Only public responses are cacheable - token/role responses are
67
67
  // caller-dependent. Both tags are invalidated by the service layer
68
68
  // on every write path: entry mutations bust collection:<slug>, and
69
69
  // definition edits (entries in api-endpoints) bust
@@ -4,18 +4,18 @@
4
4
  * Submissions are stored exclusively in Collections (no dual-storage).
5
5
  *
6
6
  * Endpoints (prefix: /api):
7
- * GET /forms — admin: list all forms
8
- * POST /forms — admin: create new form
9
- * GET /forms/:slug — admin: get form definition
10
- * GET /forms/:slug/public — public: get form (no actions block)
11
- * PUT /forms/:slug — admin: update form definition
12
- * DELETE /forms/:slug — admin: delete form
13
- * GET /forms/:slug/submissions — admin: list submissions from collection
14
- * GET /forms/:slug/submissions/export — admin: CSV export from collection
15
- * DELETE /forms/:slug/submissions — admin: clear all submissions
16
- * DELETE /forms/:slug/submissions/:id — admin: delete one submission
17
- * POST /forms/submit/:slug — public: accept submission
18
- * POST /forms/test-email — admin: send test email
7
+ * GET /forms - admin: list all forms
8
+ * POST /forms - admin: create new form
9
+ * GET /forms/:slug - admin: get form definition
10
+ * GET /forms/:slug/public - public: get form (no actions block)
11
+ * PUT /forms/:slug - admin: update form definition
12
+ * DELETE /forms/:slug - admin: delete form
13
+ * GET /forms/:slug/submissions - admin: list submissions from collection
14
+ * GET /forms/:slug/submissions/export - admin: CSV export from collection
15
+ * DELETE /forms/:slug/submissions - admin: clear all submissions
16
+ * DELETE /forms/:slug/submissions/:id - admin: delete one submission
17
+ * POST /forms/submit/:slug - public: accept submission
18
+ * POST /forms/test-email - admin: send test email
19
19
  */
20
20
  import {createRequire} from 'module';
21
21
  import {fileURLToPath} from 'url';
@@ -40,7 +40,7 @@ import {v4 as uuidv4} from 'uuid';
40
40
  /**
41
41
  * Detect form↔collection mismatches at save time so admins know BEFORE
42
42
  * users hit silent rejection. Returns an array of human-readable warning
43
- * strings — empty when the form lines up cleanly with its target collection.
43
+ * strings - empty when the form lines up cleanly with its target collection.
44
44
  *
45
45
  * Checks performed:
46
46
  * - Target collection exists (otherwise every submission goes nowhere)
@@ -63,7 +63,7 @@ async function detectFormCollectionMismatch(form) {
63
63
  return warnings;
64
64
  }
65
65
  if (!collection) {
66
- warnings.push(`Target collection "${targetSlug}" does not exist — submissions will fail. Either create the collection or change the form's target.`);
66
+ warnings.push(`Target collection "${targetSlug}" does not exist - submissions will fail. Either create the collection or change the form's target.`);
67
67
  return warnings;
68
68
  }
69
69
 
@@ -145,7 +145,7 @@ function validateUpload(fieldCfg, part, size) {
145
145
  * `content/media/`, and replaced by a `{ url, name, size, mime }` reference.
146
146
  *
147
147
  * Throws on validation failure (caller handles HTTP 400). Files are streamed
148
- * into memory up to fastify-multipart's configured limit — fine for the
148
+ * into memory up to fastify-multipart's configured limit - fine for the
149
149
  * resume-pdf / portrait-photo scale; large uploads should still use the admin
150
150
  * media flow.
151
151
  *
@@ -163,7 +163,7 @@ async function parseMultipartForm(request, form) {
163
163
  if (part.type === 'file') {
164
164
  const fieldCfg = fileFields.get(part.fieldname);
165
165
  if (!fieldCfg) {
166
- // Unknown file field — drain and skip rather than 400, so a
166
+ // Unknown file field - drain and skip rather than 400, so a
167
167
  // form that gains a file input later doesn't reject older
168
168
  // submissions in-flight from a stale page.
169
169
  for await (const _ of part.file) { /* drain */ }
@@ -174,7 +174,7 @@ async function parseMultipartForm(request, form) {
174
174
  const buffer = Buffer.concat(chunks);
175
175
  const err = validateUpload(fieldCfg, part, buffer.length);
176
176
  if (err) throw new Error(err);
177
- if (buffer.length === 0) continue; // empty file input — skip
177
+ if (buffer.length === 0) continue; // empty file input - skip
178
178
  const saved = await saveMedia(safeFilename(part.filename), buffer);
179
179
  out[part.fieldname] = {
180
180
  url: saved.url,
@@ -183,7 +183,7 @@ async function parseMultipartForm(request, form) {
183
183
  mime: part.mimetype
184
184
  };
185
185
  } else {
186
- // Text field — last value wins on duplicates (browser-standard).
186
+ // Text field - last value wins on duplicates (browser-standard).
187
187
  out[part.fieldname] = part.value;
188
188
  }
189
189
  }
@@ -237,7 +237,7 @@ export async function formsRoutes(fastify) {
237
237
  const canDelete = { preHandler: [authenticate, requirePermission('collections', 'delete')] };
238
238
 
239
239
  // -----------------------------------------------------------------------
240
- // GET /forms — list all form definitions with submission counts
240
+ // GET /forms - list all form definitions with submission counts
241
241
  // -----------------------------------------------------------------------
242
242
  fastify.get('/forms', canRead, async (request) => {
243
243
  const {canSeeArtefact} = await import('../../services/projects.js');
@@ -245,7 +245,7 @@ export async function formsRoutes(fastify) {
245
245
  const result = await Promise.all(forms.map(async form => {
246
246
  let submissionCount = 0;
247
247
  try {
248
- // listEntries returns { entries, total, page, limit } — total is the
248
+ // listEntries returns { entries, total, page, limit } - total is the
249
249
  // unpaginated count which is what we want for the badge here.
250
250
  const r = await listEntries(form.slug, { limit: 0 });
251
251
  submissionCount = r.total ?? (r.entries || []).length;
@@ -263,7 +263,7 @@ export async function formsRoutes(fastify) {
263
263
  });
264
264
 
265
265
  // -----------------------------------------------------------------------
266
- // POST /forms — create new form
266
+ // POST /forms - create new form
267
267
  // -----------------------------------------------------------------------
268
268
  fastify.post('/forms', canCreate, async (request, reply) => {
269
269
  const { title, slug: rawSlug } = request.body || {};
@@ -280,7 +280,7 @@ export async function formsRoutes(fastify) {
280
280
  await readForm(slug);
281
281
  return reply.status(409).send({ error: `A form with slug "${slug}" already exists.` });
282
282
  } catch {
283
- // Does not exist — good
283
+ // Does not exist - good
284
284
  }
285
285
 
286
286
  const now = new Date().toISOString();
@@ -333,7 +333,7 @@ export async function formsRoutes(fastify) {
333
333
  });
334
334
 
335
335
  // -----------------------------------------------------------------------
336
- // GET /forms/:slug — get single form (admin, includes actions)
336
+ // GET /forms/:slug - get single form (admin, includes actions)
337
337
  // -----------------------------------------------------------------------
338
338
  fastify.get('/forms/:slug', canRead, async (request, reply) => {
339
339
  let form;
@@ -350,7 +350,7 @@ export async function formsRoutes(fastify) {
350
350
  });
351
351
 
352
352
  // -----------------------------------------------------------------------
353
- // GET /forms/:slug/public — get form for public rendering (no actions)
353
+ // GET /forms/:slug/public - get form for public rendering (no actions)
354
354
  // -----------------------------------------------------------------------
355
355
  fastify.get('/forms/:slug/public', async (request, reply) => {
356
356
  try {
@@ -363,7 +363,7 @@ export async function formsRoutes(fastify) {
363
363
  });
364
364
 
365
365
  // -----------------------------------------------------------------------
366
- // PUT /forms/:slug — update form definition
366
+ // PUT /forms/:slug - update form definition
367
367
  // -----------------------------------------------------------------------
368
368
  fastify.put('/forms/:slug', canUpdate, async (request, reply) => {
369
369
  const { slug } = request.params;
@@ -386,14 +386,14 @@ export async function formsRoutes(fastify) {
386
386
 
387
387
  // Non-blocking warnings: check the form's fields against its target
388
388
  // collection's required fields. Missing-required-on-form would cause
389
- // every submission to fail server-side validation — admin gets a
389
+ // every submission to fail server-side validation - admin gets a
390
390
  // heads-up here so they can fix it BEFORE users hit silent rejection.
391
391
  const warnings = await detectFormCollectionMismatch(updated);
392
392
  return warnings.length ? { ...updated, warnings } : updated;
393
393
  });
394
394
 
395
395
  // -----------------------------------------------------------------------
396
- // DELETE /forms/:slug — delete form
396
+ // DELETE /forms/:slug - delete form
397
397
  // -----------------------------------------------------------------------
398
398
  fastify.delete('/forms/:slug', canDelete, async (request, reply) => {
399
399
  const { slug } = request.params;
@@ -406,7 +406,7 @@ export async function formsRoutes(fastify) {
406
406
  });
407
407
 
408
408
  // -----------------------------------------------------------------------
409
- // GET /forms/:slug/submissions — list submissions from collection
409
+ // GET /forms/:slug/submissions - list submissions from collection
410
410
  // -----------------------------------------------------------------------
411
411
  fastify.get('/forms/:slug/submissions', canRead, async (request, reply) => {
412
412
  const { slug } = request.params;
@@ -420,19 +420,19 @@ export async function formsRoutes(fastify) {
420
420
  const targetSlug = (collectionAction?.enabled && collectionAction.slug) ? collectionAction.slug : slug;
421
421
 
422
422
  // A form whose backing collection has not been provisioned yet simply
423
- // has no submissions — return an empty list rather than a 404 so the
423
+ // has no submissions - return an empty list rather than a 404 so the
424
424
  // admin view loads cleanly instead of erroring.
425
425
  const collection = await getCollection(targetSlug);
426
426
  if (!collection) return [];
427
427
 
428
- // listEntries returns { entries, total, page, limit } — unwrap to the
428
+ // listEntries returns { entries, total, page, limit } - unwrap to the
429
429
  // array. Newest-first is the conventional display order.
430
430
  const { entries } = await listEntries(targetSlug, { limit: 0 });
431
431
  return [...entries].reverse();
432
432
  });
433
433
 
434
434
  // -----------------------------------------------------------------------
435
- // GET /forms/:slug/submissions/export — CSV download from collection
435
+ // GET /forms/:slug/submissions/export - CSV download from collection
436
436
  // -----------------------------------------------------------------------
437
437
  fastify.get('/forms/:slug/submissions/export', canRead, async (request, reply) => {
438
438
  const { slug } = request.params;
@@ -456,7 +456,7 @@ export async function formsRoutes(fastify) {
456
456
  });
457
457
 
458
458
  // -----------------------------------------------------------------------
459
- // GET /forms/:slug/submissions/export/json — JSON export from collection
459
+ // GET /forms/:slug/submissions/export/json - JSON export from collection
460
460
  // -----------------------------------------------------------------------
461
461
  fastify.get('/forms/:slug/submissions/export/json', canRead, async (request, reply) => {
462
462
  const { slug } = request.params;
@@ -479,7 +479,7 @@ export async function formsRoutes(fastify) {
479
479
  });
480
480
 
481
481
  // -----------------------------------------------------------------------
482
- // DELETE /forms/:slug/submissions — clear all submissions
482
+ // DELETE /forms/:slug/submissions - clear all submissions
483
483
  // -----------------------------------------------------------------------
484
484
  fastify.delete('/forms/:slug/submissions', canDelete, async (request, reply) => {
485
485
  const { slug } = request.params;
@@ -492,7 +492,7 @@ export async function formsRoutes(fastify) {
492
492
  });
493
493
 
494
494
  // -----------------------------------------------------------------------
495
- // DELETE /forms/:slug/submissions/:id — delete one submission
495
+ // DELETE /forms/:slug/submissions/:id - delete one submission
496
496
  // -----------------------------------------------------------------------
497
497
  fastify.delete('/forms/:slug/submissions/:id', canDelete, async (request, reply) => {
498
498
  const { slug, id } = request.params;
@@ -505,12 +505,12 @@ export async function formsRoutes(fastify) {
505
505
  });
506
506
 
507
507
  /*
508
- * POST /forms/submit/:slug — public form submission.
508
+ * POST /forms/submit/:slug - public form submission.
509
509
  *
510
510
  * Authentication is OPTIONAL: when a valid JWT is present the submitter
511
511
  * is captured as the entry's `createdBy` and passed into any triggered
512
512
  * action's template context (as `{{user.id}}`, `{{user.email}}`, etc.).
513
- * Anonymous submissions still work — `createdBy` is null and actions
513
+ * Anonymous submissions still work - `createdBy` is null and actions
514
514
  * receive an empty user. This is what makes the apply/bookmark patterns
515
515
  * work without forcing every form to be auth-only:
516
516
  *
@@ -527,7 +527,7 @@ export async function formsRoutes(fastify) {
527
527
  return reply.status(404).send({ error: 'Form not found.' });
528
528
  }
529
529
 
530
- // Multipart parsing — when the request is multipart (file uploads),
530
+ // Multipart parsing - when the request is multipart (file uploads),
531
531
  // we walk parts ourselves: text fields go into `body`, file parts are
532
532
  // validated against the form's field config and saved to /content/media,
533
533
  // then their { url, name, size, mime } reference object lands in `body`
@@ -542,7 +542,7 @@ export async function formsRoutes(fastify) {
542
542
  }
543
543
  const settings = form.settings || {};
544
544
 
545
- // Best-effort auth — never reject, just enrich.
545
+ // Best-effort auth - never reject, just enrich.
546
546
  let submittingUser = null;
547
547
  try {
548
548
  const decoded = await request.jwtVerify();
@@ -556,12 +556,12 @@ export async function formsRoutes(fastify) {
556
556
  }
557
557
  } catch { /* anonymous submission */ }
558
558
 
559
- // Honeypot check — silently accept if filled (bot detected)
559
+ // Honeypot check - silently accept if filled (bot detected)
560
560
  if (settings.honeypot && body._hp) {
561
561
  return { ok: true, message: settings.successMessage, redirect: settings.successRedirect || null };
562
562
  }
563
563
 
564
- // Timing check — silently accept if submitted too fast (< 2 s, likely a bot)
564
+ // Timing check - silently accept if submitted too fast (< 2 s, likely a bot)
565
565
  if (settings.honeypot && body._t) {
566
566
  const elapsed = Date.now() - Number(body._t);
567
567
  if (!Number.isNaN(elapsed) && elapsed < 2000) {
@@ -614,7 +614,7 @@ export async function formsRoutes(fastify) {
614
614
  return reply.status(429).send({ error: 'Too many submissions. Please try again later.' });
615
615
  }
616
616
 
617
- // Build submission data — only include visible fields
617
+ // Build submission data - only include visible fields
618
618
  const data = {};
619
619
  for (const field of form.fields || []) {
620
620
  if (field.type === 'page-break' || field.type === 'spacer') continue;
@@ -630,9 +630,9 @@ export async function formsRoutes(fastify) {
630
630
  // CRITICAL: collection write failure is the difference between "your
631
631
  // submission was saved" and "your submission disappeared into thin air".
632
632
  // We MUST surface the error to the user rather than logging a warning
633
- // and returning success — anything else is a silent-data-loss bug.
633
+ // and returning success - anything else is a silent-data-loss bug.
634
634
  // (Email / webhook / action failures downstream are still treated as
635
- // non-fatal — they fire AFTER the entry is persisted, so even a partial
635
+ // non-fatal - they fire AFTER the entry is persisted, so even a partial
636
636
  // delivery has the original submission safely stored.)
637
637
  const collectionAction = form.actions?.collection;
638
638
  const targetSlug = (collectionAction?.enabled && collectionAction.slug) ? collectionAction.slug : slug;
@@ -653,12 +653,12 @@ export async function formsRoutes(fastify) {
653
653
  createdBy: submittingUser?.id || null
654
654
  });
655
655
  } else {
656
- // Could not resolve or provision a target — e.g. collection
656
+ // Could not resolve or provision a target - e.g. collection
657
657
  // storage is explicitly disabled with no slug. Fail loudly
658
658
  // rather than silently lose submissions.
659
659
  fastify.log.warn(`[forms] Submission for "${slug}" had no target collection "${targetSlug}"`);
660
660
  return reply.status(500).send({
661
- error: `Submission not saved — target collection "${targetSlug}" does not exist. Please contact the site administrator.`
661
+ error: `Submission not saved - target collection "${targetSlug}" does not exist. Please contact the site administrator.`
662
662
  });
663
663
  }
664
664
  } catch (err) {
@@ -706,7 +706,7 @@ export async function formsRoutes(fastify) {
706
706
  }
707
707
  }
708
708
 
709
- // CMS Action trigger — forwards the authenticated submitter so
709
+ // CMS Action trigger - forwards the authenticated submitter so
710
710
  // action steps can interpolate {{user.id}}, {{user.email}}, etc.
711
711
  // (Anonymous submissions still trigger the action with user={}.)
712
712
  const actionSlug = form.settings?.actionSlug;
@@ -735,7 +735,7 @@ export async function formsRoutes(fastify) {
735
735
  });
736
736
 
737
737
  // -----------------------------------------------------------------------
738
- // POST /forms/test-email — send a test email
738
+ // POST /forms/test-email - send a test email
739
739
  // -----------------------------------------------------------------------
740
740
  fastify.post('/forms/test-email', { preHandler: [authenticate, requireAdmin] }, async (request, reply) => {
741
741
  const smtp = getConfig('site').smtp || {};
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Menu locations API
3
3
  *
4
- * GET /api/menu-locations — current slot → menu-slug map
5
- * PUT /api/menu-locations — replace the map (validates against registry + menus)
6
- * GET /api/menu-locations/registry — list registered slots (core + plugin-contributed)
4
+ * GET /api/menu-locations - current slot → menu-slug map
5
+ * PUT /api/menu-locations - replace the map (validates against registry + menus)
6
+ * GET /api/menu-locations/registry - list registered slots (core + plugin-contributed)
7
7
  *
8
8
  * Auth middlewares are accepted as options so tests can supply no-ops without
9
9
  * needing to register `@fastify/jwt` + seed the roles cache. In production the
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Menus API
3
3
  *
4
- * GET /api/menus — list (metadata only)
5
- * GET /api/menus/:slug — full menu
6
- * POST /api/menus — create
7
- * PUT /api/menus/:slug — replace
8
- * DELETE /api/menus/:slug — delete (409 if mapped to a slot)
9
- * POST /api/menus/:slug/duplicate — copy as <slug>-copy
4
+ * GET /api/menus - list (metadata only)
5
+ * GET /api/menus/:slug - full menu
6
+ * POST /api/menus - create
7
+ * PUT /api/menus/:slug - replace
8
+ * DELETE /api/menus/:slug - delete (409 if mapped to a slot)
9
+ * POST /api/menus/:slug/duplicate - copy as <slug>-copy
10
10
  *
11
11
  * Auth middlewares are accepted as options so tests can supply no-ops without
12
12
  * needing to register `@fastify/jwt` + seed the roles cache. In production the
@@ -93,7 +93,7 @@ export async function menusRoutes(fastify, opts = {}) {
93
93
  const slot = Object.entries(map).find(([, slug]) => slug === request.params.slug)?.[0];
94
94
  if (slot) {
95
95
  return reply.status(409).send({
96
- error: `Cannot delete — menu is mapped to slot "${slot}". Unmap it first.`
96
+ error: `Cannot delete - menu is mapped to slot "${slot}". Unmap it first.`
97
97
  });
98
98
  }
99
99
  const ok = await deleteMenu(request.params.slug);