domma-cms 0.43.1 → 0.45.0

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 (251) 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 +25 -25
  57. package/admin/js/templates/form-editor.html +5 -5
  58. package/admin/js/templates/login.html +5 -5
  59. package/admin/js/templates/menu-editor.html +10 -10
  60. package/admin/js/templates/navigation.html +11 -11
  61. package/admin/js/templates/notifications.html +1 -1
  62. package/admin/js/templates/page-editor.html +4 -4
  63. package/admin/js/templates/pro-docs.html +14 -14
  64. package/admin/js/templates/role-editor.html +1 -1
  65. package/admin/js/templates/settings.html +10 -10
  66. package/admin/js/templates/tutorials.html +3 -3
  67. package/admin/js/templates/view-editor.html +15 -15
  68. package/admin/js/views/action-editor.js +1 -1
  69. package/admin/js/views/actions-list.js +1 -1
  70. package/admin/js/views/api-endpoint-editor.js +2 -2
  71. package/admin/js/views/api-endpoints.js +2 -2
  72. package/admin/js/views/api-tokens.js +3 -3
  73. package/admin/js/views/blocks.js +2 -2
  74. package/admin/js/views/collection-editor.js +1 -1
  75. package/admin/js/views/collection-entries.js +1 -1
  76. package/admin/js/views/collections.js +1 -1
  77. package/admin/js/views/component-editor.js +1 -1
  78. package/admin/js/views/components.js +4 -4
  79. package/admin/js/views/dashboard/widgets/cache.js +1 -1
  80. package/admin/js/views/dashboard/widgets/journeys.js +1 -1
  81. package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
  82. package/admin/js/views/form-editor.js +7 -7
  83. package/admin/js/views/form-submissions.js +1 -1
  84. package/admin/js/views/forms.js +1 -1
  85. package/admin/js/views/layouts.js +1 -1
  86. package/admin/js/views/login.js +2 -2
  87. package/admin/js/views/menu-editor.js +3 -3
  88. package/admin/js/views/menu-locations.js +1 -1
  89. package/admin/js/views/menus.js +3 -3
  90. package/admin/js/views/navigation.js +8 -8
  91. package/admin/js/views/page-editor.js +40 -40
  92. package/admin/js/views/pages.js +3 -3
  93. package/admin/js/views/plugin-code.js +2 -2
  94. package/admin/js/views/plugins.js +1 -1
  95. package/admin/js/views/project-settings.js +1 -1
  96. package/admin/js/views/projects.js +1 -1
  97. package/admin/js/views/roles.js +1 -1
  98. package/admin/js/views/user-editor.js +1 -1
  99. package/admin/js/views/users.js +3 -3
  100. package/admin/js/views/view-editor.js +1 -1
  101. package/admin/js/views/view-preview.js +1 -1
  102. package/admin/js/views/views-list.js +1 -1
  103. package/bin/cli.js +8 -8
  104. package/bin/lib/config-merge.js +5 -5
  105. package/bin/update.js +16 -16
  106. package/config/site.json +86 -86
  107. package/package.json +2 -2
  108. package/plugins/_template/admin/views/index.js +1 -1
  109. package/plugins/_template/config.js +1 -1
  110. package/plugins/_template/plugin.js +1 -1
  111. package/plugins/analytics/admin/templates/analytics.html +3 -3
  112. package/plugins/analytics/admin/views/analytics.js +1 -1
  113. package/plugins/analytics/config.js +1 -1
  114. package/plugins/analytics/plugin.js +4 -4
  115. package/plugins/analytics/plugin.json +1 -1
  116. package/plugins/analytics/public/inject-body.html +2 -2
  117. package/plugins/analytics/public/inject-head.html +1 -1
  118. package/plugins/blog/admin/templates/blog.html +4 -4
  119. package/plugins/blog/admin/views/blog.js +3 -3
  120. package/plugins/blog/admin/views/categories.js +2 -2
  121. package/plugins/blog/admin/views/comments.js +4 -4
  122. package/plugins/blog/admin/views/post-editor.js +1 -1
  123. package/plugins/blog/plugin.js +2 -2
  124. package/plugins/blog/plugin.json +4 -4
  125. package/plugins/contacts/admin/views/contacts.js +8 -8
  126. package/plugins/contacts/plugin.js +4 -4
  127. package/plugins/contacts/plugin.json +1 -1
  128. package/plugins/invoice/admin/templates/editor.html +6 -6
  129. package/plugins/invoice/admin/templates/index.html +4 -4
  130. package/plugins/invoice/admin/views/editor.js +3 -3
  131. package/plugins/invoice/admin/views/index.js +4 -4
  132. package/plugins/invoice/admin/views/issuers.js +1 -1
  133. package/plugins/invoice/admin/views/party-view.js +3 -3
  134. package/plugins/invoice/admin/views/receivers.js +1 -1
  135. package/plugins/invoice/config.js +1 -1
  136. package/plugins/invoice/plugin.js +1 -1
  137. package/plugins/invoice/plugin.json +4 -4
  138. package/plugins/notes/admin/views/notes.js +4 -4
  139. package/plugins/notes/plugin.js +1 -1
  140. package/plugins/notes/plugin.json +1 -1
  141. package/plugins/site-search/admin/templates/site-search.html +3 -3
  142. package/plugins/site-search/admin/views/site-search.js +1 -1
  143. package/plugins/site-search/config.js +1 -1
  144. package/plugins/site-search/plugin.js +4 -4
  145. package/plugins/site-search/plugin.json +1 -1
  146. package/plugins/surveys/admin/templates/results.html +1 -1
  147. package/plugins/surveys/admin/templates/survey-editor.html +4 -4
  148. package/plugins/surveys/admin/views/audience.js +6 -6
  149. package/plugins/surveys/admin/views/results.js +3 -3
  150. package/plugins/surveys/admin/views/survey-editor.js +1 -1
  151. package/plugins/surveys/admin/views/surveys.js +2 -2
  152. package/plugins/surveys/plugin.js +1 -1
  153. package/plugins/surveys/plugin.json +4 -4
  154. package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
  155. package/plugins/theme-switcher/plugin.json +3 -3
  156. package/plugins/theme-switcher/public/inject-body.html +28 -28
  157. package/plugins/theme-switcher/public/inject-head.html +1 -1
  158. package/plugins/todo/plugin.js +1 -1
  159. package/public/css/forms.css +1 -1
  160. package/public/js/collection-browser.js +1 -1
  161. package/public/js/collection-context.js +1 -1
  162. package/public/js/form-logic-engine.js +1 -1
  163. package/public/js/forms.js +2 -1
  164. package/public/js/site.js +1 -1
  165. package/scripts/build.js +100 -6
  166. package/scripts/create-plugin.js +2 -2
  167. package/scripts/fresh.js +2 -2
  168. package/scripts/gen-instance-secret.js +1 -1
  169. package/scripts/pro.js +12 -12
  170. package/scripts/reset.js +2 -2
  171. package/scripts/setup.js +15 -15
  172. package/scripts/users.js +5 -5
  173. package/scripts/verify-assets.mjs +6 -6
  174. package/server/config.js +1 -1
  175. package/server/middleware/auth.js +11 -11
  176. package/server/middleware/managerAuth.js +3 -3
  177. package/server/routes/api/actions.js +4 -4
  178. package/server/routes/api/api-endpoints.js +7 -7
  179. package/server/routes/api/api-tokens.js +5 -5
  180. package/server/routes/api/auth.js +10 -10
  181. package/server/routes/api/blocks.js +3 -3
  182. package/server/routes/api/collections.js +18 -18
  183. package/server/routes/api/dashboard.js +5 -5
  184. package/server/routes/api/effects.js +2 -2
  185. package/server/routes/api/endpoints-public.js +5 -5
  186. package/server/routes/api/forms.js +47 -47
  187. package/server/routes/api/menu-locations.js +3 -3
  188. package/server/routes/api/menus.js +7 -7
  189. package/server/routes/api/notifications.js +2 -2
  190. package/server/routes/api/pages.js +14 -2
  191. package/server/routes/api/plugin-marketplace.js +3 -3
  192. package/server/routes/api/plugins.js +6 -6
  193. package/server/routes/api/projects.js +9 -9
  194. package/server/routes/api/scaffold.js +7 -7
  195. package/server/routes/api/settings.js +6 -6
  196. package/server/routes/api/sidebar.js +1 -1
  197. package/server/routes/api/users.js +5 -5
  198. package/server/routes/api/versions.js +1 -1
  199. package/server/routes/api/views.js +1 -1
  200. package/server/routes/docs-public.js +3 -3
  201. package/server/routes/public.js +14 -14
  202. package/server/server.js +34 -16
  203. package/server/services/actions.js +19 -19
  204. package/server/services/adapters/FileAdapter.js +2 -2
  205. package/server/services/adapters/MongoAdapter.js +5 -5
  206. package/server/services/apiEndpoints.js +15 -15
  207. package/server/services/apiTokens.js +7 -7
  208. package/server/services/blocks.js +13 -13
  209. package/server/services/cache/drivers/MemoryDriver.js +3 -3
  210. package/server/services/cache/drivers/NoneDriver.js +1 -1
  211. package/server/services/cache/index.js +5 -5
  212. package/server/services/collections.js +14 -14
  213. package/server/services/components.js +9 -9
  214. package/server/services/connectionManager.js +4 -4
  215. package/server/services/content.js +25 -2
  216. package/server/services/docs.js +14 -14
  217. package/server/services/email.js +1 -1
  218. package/server/services/filterEngine.js +6 -6
  219. package/server/services/forms.js +18 -10
  220. package/server/services/health.js +1 -1
  221. package/server/services/hooks.js +7 -7
  222. package/server/services/images.js +4 -4
  223. package/server/services/managerClient.js +1 -1
  224. package/server/services/markdown.js +79 -79
  225. package/server/services/menuRender.js +5 -5
  226. package/server/services/menus-migration.js +3 -3
  227. package/server/services/menus.js +31 -31
  228. package/server/services/permissionRegistry.js +1 -1
  229. package/server/services/pluginFiles.js +5 -5
  230. package/server/services/pluginInstaller.js +17 -17
  231. package/server/services/pluginScaffold.js +1 -1
  232. package/server/services/plugins.js +17 -17
  233. package/server/services/presetCollections.js +2 -2
  234. package/server/services/projects.js +35 -26
  235. package/server/services/recipes/contact-list.json +1 -1
  236. package/server/services/recipes/onboarding.json +8 -8
  237. package/server/services/references.js +8 -8
  238. package/server/services/renderer.js +9 -9
  239. package/server/services/roles.js +9 -9
  240. package/server/services/rowAccess.js +10 -10
  241. package/server/services/scaffolder.js +30 -30
  242. package/server/services/sidebar-migration.js +10 -10
  243. package/server/services/sitemap.js +1 -1
  244. package/server/services/userProfiles.js +4 -4
  245. package/server/services/userRoles.js +7 -7
  246. package/server/services/users.js +5 -5
  247. package/server/services/versions.js +3 -3
  248. package/server/services/viewPipeline.js +6 -6
  249. package/server/services/views.js +11 -11
  250. package/server/templates/page.html +5 -5
  251. package/config/navigation.json.bak +0 -31
@@ -63,7 +63,7 @@ export function requireRole(allowedRoles) {
63
63
 
64
64
  /**
65
65
  * Return a preHandler that checks the current user's role has access to a resource.
66
- * Reads from the roles cache at request time — reflects live role changes.
66
+ * Reads from the roles cache at request time - reflects live role changes.
67
67
  *
68
68
  * @param {string} resource - Resource key (e.g. 'pages', 'users')
69
69
  * @param {string} [action] - Optional action (read | create | update | delete)
@@ -80,7 +80,7 @@ export function requirePermission(resource, action) {
80
80
  }
81
81
 
82
82
  // Check ANY of the user's effective roles (primary + additional) against
83
- // the resource's permission list — union semantics so a user with both
83
+ // the resource's permission list - union semantics so a user with both
84
84
  // candidate and recruiter roles can do either role's actions.
85
85
  const allowed = getPermissionsFor(resource, action);
86
86
  const roles = getEffectiveRoles(request.user);
@@ -103,7 +103,7 @@ export function requirePermission(resource, action) {
103
103
  export {getPermissionsForRole};
104
104
 
105
105
  /**
106
- * Shorthand preHandler — admin-tier role (level ≤ 1) or above.
106
+ * Shorthand preHandler - admin-tier role (level ≤ 1) or above.
107
107
  * Matches the base role hierarchy documented in roles.js:
108
108
  * super-admin (0), admin (1), user (2).
109
109
  * Both super-admin and admin pass; regular users and anything below do not.
@@ -116,7 +116,7 @@ export async function requireAdmin(request, reply) {
116
116
  if (!request.user) {
117
117
  return reply.code(401).send({ statusCode: 401, error: 'Unauthorised', message: 'Authentication required' });
118
118
  }
119
- // Use effective level — a user with "additionalRoles: ['admin']" can do admin work
119
+ // Use effective level - a user with "additionalRoles: ['admin']" can do admin work
120
120
  // even if their primary role is something lower-privilege.
121
121
  if (getEffectiveLevel(request.user) > 1) {
122
122
  return reply.code(403).send({ statusCode: 403, error: 'Forbidden', message: 'Admin access required' });
@@ -127,7 +127,7 @@ export async function requireAdmin(request, reply) {
127
127
  * Determine whether an actor can manage a target user.
128
128
  * Managers cannot create, edit, or delete users with a lower level number (higher privilege).
129
129
  *
130
- * Accepts either user objects (preferred — uses effective level across all roles)
130
+ * Accepts either user objects (preferred - uses effective level across all roles)
131
131
  * or bare role-name strings (legacy compat). Strings are looked up via
132
132
  * `getRoleLevel`; objects via `getEffectiveLevel` so multi-role actors and
133
133
  * targets are compared by their HIGHEST-privilege role.
@@ -148,19 +148,19 @@ export function canManageUser(actor, target) {
148
148
  *
149
149
  * Visibility may be either:
150
150
  * - A single string ('public', 'private', or a role name)
151
- * - An array of role names — granted if ANY entry passes the per-role check
151
+ * - An array of role names - granted if ANY entry passes the per-role check
152
152
  *
153
153
  * Per-role semantics are unchanged: each role check passes if the visitor's
154
154
  * role level is at or above the required role (lower or equal level number).
155
155
  * 'private' resolves to super-admin only (Infinity → level 0).
156
156
  *
157
157
  * The "any of" semantics for arrays means siblings at different tiers of the
158
- * hierarchy are all granted access — e.g. `visibility: [candidate, employer]`
158
+ * hierarchy are all granted access - e.g. `visibility: [candidate, employer]`
159
159
  * lets both roles in, plus anyone more privileged than either (typically
160
160
  * admins inherit access automatically via the level comparison).
161
161
  *
162
162
  * @param {string|null} userRole - The visitor's role, or null if unauthenticated
163
- * @param {string|string[]} visibility - Required visibility — single value or array
163
+ * @param {string|string[]} visibility - Required visibility - single value or array
164
164
  * @returns {boolean} true if access is granted
165
165
  */
166
166
  export function checkVisibility(userRoleOrObj, visibility) {
@@ -168,7 +168,7 @@ export function checkVisibility(userRoleOrObj, visibility) {
168
168
 
169
169
  // Accept either a bare role name (legacy) or a user object with multi-role
170
170
  // support. When given an object we walk every effective role and grant
171
- // access if ANY satisfies — same union semantics as permissions.
171
+ // access if ANY satisfies - same union semantics as permissions.
172
172
  const roles = typeof userRoleOrObj === 'string'
173
173
  ? (userRoleOrObj ? [userRoleOrObj] : [])
174
174
  : getEffectiveRoles(userRoleOrObj);
@@ -186,7 +186,7 @@ export function checkVisibility(userRoleOrObj, visibility) {
186
186
  }
187
187
 
188
188
  /**
189
- * Internal helper — single-role visibility check.
189
+ * Internal helper - single-role visibility check.
190
190
  * Returns true if the user role is at or above the required role's level.
191
191
  *
192
192
  * @param {string} userRole - Must not be null
@@ -201,7 +201,7 @@ function checkSingleVisibility(userRole, visibility) {
201
201
  }
202
202
 
203
203
  /**
204
- * Fastify preHandler factory — gates a route by visibility level.
204
+ * Fastify preHandler factory - gates a route by visibility level.
205
205
  * Works identically to the content-page visibility system; accepts the same
206
206
  * single-string or array-of-roles syntax as checkVisibility().
207
207
  *
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Manager auth middleware — shared-secret guard for inbound requests from domma-cms-manager.
2
+ * Manager auth middleware - shared-secret guard for inbound requests from domma-cms-manager.
3
3
  * Completely separate from JWT authenticate. Do NOT mix these two auth surfaces.
4
4
  */
5
5
  import { timingSafeEqual, createHmac } from 'crypto';
6
6
 
7
7
  /**
8
- * Fastify preHandler — validates the X-Manager-Token or Authorization: Bearer header
8
+ * Fastify preHandler - validates the X-Manager-Token or Authorization: Bearer header
9
9
  * against the MANAGER_SECRET environment variable using timing-safe comparison.
10
10
  *
11
11
  * @param {import('fastify').FastifyRequest} request
@@ -26,7 +26,7 @@ export function requireManager(request, reply, done) {
26
26
  return reply.code(401).send({ error: 'Manager token required.' });
27
27
  }
28
28
 
29
- // Compare HMAC digests — always 32 bytes regardless of input length, preventing timing oracle
29
+ // Compare HMAC digests - always 32 bytes regardless of input length, preventing timing oracle
30
30
  const digest = (s) => createHmac('sha256', 'domma-cms').update(s).digest();
31
31
  if (!timingSafeEqual(digest(token), digest(secret))) {
32
32
  return reply.code(403).send({ error: 'Invalid manager token.' });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Actions API (Pro — requires MongoDB)
2
+ * Actions API (Pro - requires MongoDB)
3
3
  *
4
4
  * Admin endpoints (authenticated + actions permission):
5
5
  * GET /actions - List all action configs
@@ -43,7 +43,7 @@ export async function actionsRoutes(fastify) {
43
43
  try {
44
44
  const {canSeeArtefact} = await import('../../services/projects.js');
45
45
  const all = await listActions();
46
- // listActions returns full records with meta inline — filter directly
46
+ // listActions returns full records with meta inline - filter directly
47
47
  return all.filter(a => canSeeArtefact(request.user, a));
48
48
  } catch (err) {
49
49
  return reply.status(503).send({ error: err.message });
@@ -188,7 +188,7 @@ export async function actionsRoutes(fastify) {
188
188
 
189
189
  const user = request.user;
190
190
  const allowedRoles = action.access?.roles || ['admin', 'super-admin'];
191
- // Use effective level — a candidate-with-also-admin can run admin-tier actions.
191
+ // Use effective level - a candidate-with-also-admin can run admin-tier actions.
192
192
  const userLevel = getEffectiveLevel(user);
193
193
  const minAllowed = Math.min(...allowedRoles.map(r => getRoleLevel(r)));
194
194
 
@@ -217,7 +217,7 @@ export async function actionsRoutes(fastify) {
217
217
  * shortcode to render only the buttons that will actually succeed.
218
218
  *
219
219
  * Auth: optional. Anonymous calls return actions accessible to anonymous
220
- * users (rare — most transitions require a role). With a JWT we narrow
220
+ * users (rare - most transitions require a role). With a JWT we narrow
221
221
  * to what the signed-in user can do.
222
222
  *
223
223
  * Response: { transitions: [{slug, title, trigger, transition}, ...] }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * API Endpoints API (admin management of custom endpoint definitions)
3
3
  *
4
- * GET /api/api-endpoints — list definitions visible to the caller
5
- * GET /api/api-endpoints/:id — single definition (the builder editor)
6
- * POST /api/api-endpoints — create (400 validation, 409 duplicate path shape)
7
- * PUT /api/api-endpoints/:id — update (project binding immutable)
8
- * DELETE /api/api-endpoints/:id — delete
4
+ * GET /api/api-endpoints - list definitions visible to the caller
5
+ * GET /api/api-endpoints/:id - single definition (the builder editor)
6
+ * POST /api/api-endpoints - create (400 validation, 409 duplicate path shape)
7
+ * PUT /api/api-endpoints/:id - update (project binding immutable)
8
+ * DELETE /api/api-endpoints/:id - delete
9
9
  *
10
10
  * Endpoints are project-scoped artefacts: non-super-admin users with a
11
11
  * `projects: []` access scope only see/manage definitions in their projects.
@@ -28,7 +28,7 @@ import {getCollection} from '../../services/collections.js';
28
28
  /**
29
29
  * Scoped users must not expose data they cannot see: the collection an
30
30
  * endpoint queries is itself an artefact subject to project access scope.
31
- * Returns true when the user may use the collection (or it doesn't exist —
31
+ * Returns true when the user may use the collection (or it doesn't exist -
32
32
  * the service's validation produces the clearer 400 for that case).
33
33
  *
34
34
  * @param {object} user
@@ -96,7 +96,7 @@ export async function apiEndpointsRoutes(fastify, opts = {}) {
96
96
  if (!canSeeArtefact(request.user, {meta: {project: existing.project}})) {
97
97
  return reply.status(403).send({error: 'Access denied for this project'});
98
98
  }
99
- // The project binding is the endpoint's URL namespace — immutable.
99
+ // The project binding is the endpoint's URL namespace - immutable.
100
100
  const {project: _ignored, ...patch} = request.body || {};
101
101
  if (!await canUseCollection(request.user, patch.collection ?? existing.collection)) {
102
102
  return reply.status(403).send({error: 'Access denied for this collection'});
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * API Tokens API
3
3
  *
4
- * GET /api/api-tokens — list tokens visible to the caller (sanitised; never the hash)
5
- * POST /api/api-tokens — create; response carries the plaintext ONCE
6
- * PUT /api/api-tokens/:id — rename, enable/disable, edit scopes/expiry
7
- * DELETE /api/api-tokens/:id — revoke (immediate)
4
+ * GET /api/api-tokens - list tokens visible to the caller (sanitised; never the hash)
5
+ * POST /api/api-tokens - create; response carries the plaintext ONCE
6
+ * PUT /api/api-tokens/:id - rename, enable/disable, edit scopes/expiry
7
+ * DELETE /api/api-tokens/:id - revoke (immediate)
8
8
  *
9
9
  * Tokens are project-scoped artefacts: non-super-admin users with a
10
10
  * `projects: []` access scope only see/manage tokens in their projects.
@@ -51,7 +51,7 @@ export async function apiTokensRoutes(fastify, opts = {}) {
51
51
  try {
52
52
  const creator = request.user?.name || request.user?.email || null;
53
53
  const {entry, plaintext} = await createToken({name, project, scopes, expiresAt, createdBy: creator});
54
- // The plaintext appears here and nowhere else — only the hash is stored.
54
+ // The plaintext appears here and nowhere else - only the hash is stored.
55
55
  return reply.status(201).send({token: entry, plaintext});
56
56
  } catch (err) {
57
57
  return reply.status(400).send({error: err.message});
@@ -5,7 +5,7 @@
5
5
  * POST /api/auth/login - { email, password } → { token, refreshToken, user }
6
6
  * GET /api/auth/me - return current user from token
7
7
  * POST /api/auth/refresh - { refreshToken } → { token }
8
- * POST /api/auth/logout - { refreshToken } → { ok: true } — blacklists the refresh token
8
+ * POST /api/auth/logout - { refreshToken } → { ok: true } - blacklists the refresh token
9
9
  */
10
10
  import crypto from 'node:crypto';
11
11
  import {config, getConfig} from '../../config.js';
@@ -58,7 +58,7 @@ export async function authRoutes(fastify) {
58
58
  return { needsSetup: count === 0 };
59
59
  });
60
60
 
61
- // POST /api/auth/setup — create the very first admin (only allowed when no users exist)
61
+ // POST /api/auth/setup - create the very first admin (only allowed when no users exist)
62
62
  fastify.post('/auth/setup', async (request, reply) => {
63
63
  if (setupInProgress) {
64
64
  return reply.status(409).send({error: 'Setup already in progress'});
@@ -114,13 +114,13 @@ export async function authRoutes(fastify) {
114
114
  return { token, refreshToken, user: safeUser };
115
115
  });
116
116
 
117
- // GET /api/auth/permissions — returns the raw permissions array for the authenticated user's role
117
+ // GET /api/auth/permissions - returns the raw permissions array for the authenticated user's role
118
118
  fastify.get('/auth/permissions', {preHandler: [authenticate]}, async (request) => {
119
119
  const permissions = getPermissionsForRole(request.user.role);
120
120
  return {permissions};
121
121
  });
122
122
 
123
- // GET /api/auth/permissions-registry — returns the full permissions registry
123
+ // GET /api/auth/permissions-registry - returns the full permissions registry
124
124
  fastify.get('/auth/permissions-registry', {preHandler: [authenticate]}, async () => {
125
125
  return {groups: GROUP_ORDER, resources: REGISTRY};
126
126
  });
@@ -133,7 +133,7 @@ export async function authRoutes(fastify) {
133
133
  return {...user, profile: profileEntry?.data || {}};
134
134
  });
135
135
 
136
- // PUT /api/auth/me — self-service update (name, email, password, profile)
136
+ // PUT /api/auth/me - self-service update (name, email, password, profile)
137
137
  fastify.put('/auth/me', {preHandler: [authenticate]}, async (request, reply) => {
138
138
  const id = request.user.id;
139
139
  const {name, email, password, profile} = request.body || {};
@@ -157,10 +157,10 @@ export async function authRoutes(fastify) {
157
157
  return {...user, profile: profileEntry?.data || {}};
158
158
  });
159
159
 
160
- // POST /api/auth/forgot-password — send a password reset email (no auth)
160
+ // POST /api/auth/forgot-password - send a password reset email (no auth)
161
161
  fastify.post('/auth/forgot-password', { config: { rateLimit: { max: 3, timeWindow: '1 hour' } } }, async (request) => {
162
162
  const {email} = request.body || {};
163
- if (!email) return {ok: true}; // always ok — prevents enumeration
163
+ if (!email) return {ok: true}; // always ok - prevents enumeration
164
164
 
165
165
  try {
166
166
  const user = await getUserByEmail(email);
@@ -195,7 +195,7 @@ export async function authRoutes(fastify) {
195
195
  <body style="font-family:sans-serif;max-width:560px;margin:0 auto;padding:24px;">
196
196
  <h2 style="color:#333;">Password Reset</h2>
197
197
  <p>Hi ${user.name},</p>
198
- <p>We received a request to reset your password. Click the link below — it expires in 1 hour.</p>
198
+ <p>We received a request to reset your password. Click the link below - it expires in 1 hour.</p>
199
199
  <p style="margin:24px 0;">
200
200
  <a href="${resetUrl}" style="background:#5b8cff;color:#fff;padding:12px 24px;border-radius:6px;text-decoration:none;display:inline-block;">Reset Password</a>
201
201
  </p>
@@ -212,7 +212,7 @@ export async function authRoutes(fastify) {
212
212
  return {ok: true};
213
213
  });
214
214
 
215
- // POST /api/auth/reset-password — consume token and set new password (no auth)
215
+ // POST /api/auth/reset-password - consume token and set new password (no auth)
216
216
  fastify.post('/auth/reset-password', { config: { rateLimit: { max: 5, timeWindow: '1 minute' } } }, async (request, reply) => {
217
217
  const {token, password} = request.body || {};
218
218
  if (!token || !password) {
@@ -237,7 +237,7 @@ export async function authRoutes(fastify) {
237
237
  return {ok: true};
238
238
  });
239
239
 
240
- // POST /api/auth/logout — blacklists the refresh token (fire-and-forget safe: no auth required)
240
+ // POST /api/auth/logout - blacklists the refresh token (fire-and-forget safe: no auth required)
241
241
  fastify.post('/auth/logout', async (request) => {
242
242
  const {refreshToken} = request.body || {};
243
243
  if (refreshToken) blacklistedRefreshTokens.add(refreshToken);
@@ -73,7 +73,7 @@ export async function blocksRoutes(fastify) {
73
73
  }
74
74
  });
75
75
 
76
- // Import a bundle — creates or updates a block from a .dmblock.json payload.
76
+ // Import a bundle - creates or updates a block from a .dmblock.json payload.
77
77
  // Pass { overwrite: true } to replace an existing block by the same name;
78
78
  // without it, a collision returns 409 so the caller can confirm.
79
79
  fastify.post('/blocks/import', canUpdate, async (request, reply) => {
@@ -90,7 +90,7 @@ export async function blocksRoutes(fastify) {
90
90
  return reply.status(400).send({error: 'Bundle is missing html content'});
91
91
  }
92
92
 
93
- // Collision check — only when the caller hasn't explicitly opted in to overwrite.
93
+ // Collision check - only when the caller hasn't explicitly opted in to overwrite.
94
94
  if (!overwrite) {
95
95
  try {
96
96
  await getBlock(name);
@@ -98,7 +98,7 @@ export async function blocksRoutes(fastify) {
98
98
  } catch (err) {
99
99
  if (err.code === 'INVALID_NAME') return reply.status(400).send({error: err.message});
100
100
  if (err.code !== 'ENOENT') throw err;
101
- // ENOENT — no collision, proceed
101
+ // ENOENT - no collision, proceed
102
102
  }
103
103
  }
104
104
 
@@ -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