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
package/server/server.js CHANGED
@@ -38,7 +38,7 @@ const DOMMA_DIST = path.join(dommaPackageDir, 'public', 'dist');
38
38
 
39
39
  const { server: serverConfig, auth: authConfig } = config;
40
40
 
41
- // Validate JWT_SECRET before starting — prevents silent auth failures
41
+ // Validate JWT_SECRET before starting - prevents silent auth failures
42
42
  const JWT_SECRET = process.env.JWT_SECRET;
43
43
  if (!JWT_SECRET || JWT_SECRET === 'CHANGE_ME' || JWT_SECRET.length < 32) {
44
44
  console.error('');
@@ -49,7 +49,7 @@ if (!JWT_SECRET || JWT_SECRET === 'CHANGE_ME' || JWT_SECRET.length < 32) {
49
49
  process.exit(1);
50
50
  }
51
51
 
52
- // MANAGER_SECRET is optional — only needed when domma-cms-manager pushes notifications.
52
+ // MANAGER_SECRET is optional - only needed when domma-cms-manager pushes notifications.
53
53
  // Warn if set but insecure; silently accept if absent (manager push is disabled).
54
54
  const MANAGER_SECRET = process.env.MANAGER_SECRET;
55
55
  if (MANAGER_SECRET && MANAGER_SECRET.length < 32) {
@@ -92,7 +92,7 @@ await app.register(helmet, {
92
92
  }
93
93
  },
94
94
  crossOriginEmbedderPolicy: false, // allow embedding images/resources
95
- hsts: false, // disable HSTS — server runs HTTP only; HSTS would force browser to https
95
+ hsts: false, // disable HSTS - server runs HTTP only; HSTS would force browser to https
96
96
  });
97
97
 
98
98
  await app.register(jwt, { secret: process.env.JWT_SECRET });
@@ -102,7 +102,7 @@ await app.register(rateLimit, {
102
102
  global: true, // apply default limit to all routes; stricter per-route limits override this
103
103
  max: 500,
104
104
  timeWindow: '1 minute',
105
- // Loopback is always exempt — local admin use should never be rate-limited.
105
+ // Loopback is always exempt - local admin use should never be rate-limited.
106
106
  // When behind a reverse proxy (TRUST_PROXY), the proxy's loopback is also exempt.
107
107
  allowList: ['127.0.0.1', '::1', '::ffff:127.0.0.1'],
108
108
  });
@@ -133,11 +133,11 @@ await app.register(staticPlugin, {
133
133
  decorateReply: false
134
134
  });
135
135
 
136
- // Serve admin panel assets — no-store on JS/CSS so ESM module URLs are never
136
+ // Serve admin panel assets - no-store on JS/CSS so ESM module URLs are never
137
137
  // cached. `no-cache` revalidates but browsers may keep the parsed ESM module
138
138
  // keyed by URL across reloads, which means a hard refresh can still load the
139
139
  // stale code path. `no-store` skips the cache entirely. Other admin assets
140
- // (images, html) keep no-cache. Local-only impact — admin is not public.
140
+ // (images, html) keep no-cache. Local-only impact - admin is not public.
141
141
  await app.register(staticPlugin, {
142
142
  root: path.join(ROOT, 'admin'),
143
143
  prefix: '/admin/',
@@ -162,7 +162,7 @@ await fs.mkdir(pluginsDir, { recursive: true });
162
162
  await fs.mkdir(blocksDir, {recursive: true});
163
163
 
164
164
  // ---------------------------------------------------------------------------
165
- // Pro feature — optional MongoDB connections
165
+ // Pro feature - optional MongoDB connections
166
166
  // ---------------------------------------------------------------------------
167
167
 
168
168
  try {
@@ -173,11 +173,11 @@ try {
173
173
  app.addHook('onClose', shutdown);
174
174
  }
175
175
  } catch {
176
- // No connections.json or empty — pure file-based mode (free version).
176
+ // No connections.json or empty - pure file-based mode (free version).
177
177
  }
178
178
 
179
179
  // ---------------------------------------------------------------------------
180
- // Roles — seed preset collection + load into cache
180
+ // Roles - seed preset collection + load into cache
181
181
  // ---------------------------------------------------------------------------
182
182
 
183
183
  await seedRoles();
@@ -186,13 +186,13 @@ await seedUserProfiles();
186
186
  await ensureAllProfiles();
187
187
  await seedPresetCollections();
188
188
  await seedCoreProject();
189
- await seedDocsProject(); // built-in Domma Docs project — seeded disabled by default
189
+ await seedDocsProject(); // built-in Domma Docs project - seeded disabled by default
190
190
  await seedDefaultBlocks();
191
191
  await seedDefaultComponents();
192
192
  await refreshComponentTagAllowlist();
193
193
 
194
194
  // ---------------------------------------------------------------------------
195
- // Menus — one-shot migration from legacy navigation.json + site footer links
195
+ // Menus - one-shot migration from legacy navigation.json + site footer links
196
196
  // ---------------------------------------------------------------------------
197
197
 
198
198
  try {
@@ -229,7 +229,7 @@ console.log(`[cache] driver=${cache.isEnabled() ? config.cache.driver : 'disable
229
229
  // stale pages. No-op on a cold memory cache; matters for the Redis driver.
230
230
  await cache.invalidateTags(['docs']);
231
231
 
232
- // Serve uploaded media files — nosniff prevents browsers rendering spoofed content types
232
+ // Serve uploaded media files - nosniff prevents browsers rendering spoofed content types
233
233
  await app.register(staticPlugin, {
234
234
  root: mediaDir,
235
235
  prefix: '/media/',
@@ -239,7 +239,7 @@ await app.register(staticPlugin, {
239
239
  }
240
240
  });
241
241
 
242
- // Serve plugin admin/ and public/ subdirs only — block plugin.js, config.js, data/, etc.
242
+ // Serve plugin admin/ and public/ subdirs only - block plugin.js, config.js, data/, etc.
243
243
  await app.register(async function pluginStaticScope(instance) {
244
244
  instance.addHook('onRequest', (request, reply, done) => {
245
245
  const relative = request.url.replace(/^\/plugins\//, '').split('?')[0];
@@ -265,7 +265,7 @@ await app.register(async function pluginStaticScope(instance) {
265
265
  });
266
266
 
267
267
  // ---------------------------------------------------------------------------
268
- // Global error handler — prevents stack trace leaks in production
268
+ // Global error handler - prevents stack trace leaks in production
269
269
  // ---------------------------------------------------------------------------
270
270
 
271
271
  app.setErrorHandler((error, request, reply) => {
@@ -278,6 +278,24 @@ app.setErrorHandler((error, request, reply) => {
278
278
  return reply.code(statusCode).send({ error: message });
279
279
  });
280
280
 
281
+ // ---------------------------------------------------------------------------
282
+ // Admin API responses are never cacheable
283
+ // ---------------------------------------------------------------------------
284
+ // The admin SPA must always see the live state of the site - a stale list is
285
+ // indistinguishable from a missing artefact, and the user is left arguing with
286
+ // their own CMS. Everything under /api is no-store EXCEPT the two externally
287
+ // consumable surfaces, which have their own caching contracts:
288
+ // /api/v1/* - collection access by API token
289
+ // /api/x/* - API Builder endpoints
290
+ app.addHook('onSend', async (request, reply) => {
291
+ const url = request.raw.url || '';
292
+ if (!url.startsWith('/api/')) return;
293
+ if (url.startsWith('/api/v1/') || url.startsWith('/api/x/')) return;
294
+ reply.header('Cache-Control', 'no-store, must-revalidate');
295
+ reply.header('Pragma', 'no-cache');
296
+ reply.header('Expires', '0');
297
+ });
298
+
281
299
  // ---------------------------------------------------------------------------
282
300
  // Auth API Routes (no authentication required on these endpoints themselves)
283
301
  // ---------------------------------------------------------------------------
@@ -374,10 +392,10 @@ for (const [name, plugin] of Object.entries(getLoadedPlugins())) {
374
392
  }
375
393
 
376
394
  // ---------------------------------------------------------------------------
377
- // Public Site (catch-all — must be last)
395
+ // Public Site (catch-all - must be last)
378
396
  // ---------------------------------------------------------------------------
379
397
 
380
- // Domma Docs (request-time handbook) — dedicated /domma-docs(/*) prefix that
398
+ // Domma Docs (request-time handbook) - dedicated /domma-docs(/*) prefix that
381
399
  // out-prioritises the catch-all; must be registered before it.
382
400
  const { docsPublicRoutes } = await import('./routes/docs-public.js');
383
401
  await app.register(docsPublicRoutes);
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Actions Service (Pro — requires MongoDB)
2
+ * Actions Service (Pro - requires MongoDB)
3
3
  *
4
4
  * Action configs are stored in MongoDB `cms__actions` on the 'default' connection.
5
5
  * Executing an action runs a sequence of steps against a target collection entry.
6
6
  *
7
7
  * Supported step types:
8
- * updateField — overwrite one field on the source entry
9
- * deleteEntry — remove the source entry
10
- * moveToCollection — copy source entry into target collection AND delete source
11
- * createInCollection — create a new entry in another collection; source untouched
8
+ * updateField - overwrite one field on the source entry
9
+ * deleteEntry - remove the source entry
10
+ * moveToCollection - copy source entry into target collection AND delete source
11
+ * createInCollection - create a new entry in another collection; source untouched
12
12
  * (used for apply/follow/bookmark/upvote-style relations)
13
- * webhook — POST/PUT/GET to an external URL with interpolated body
14
- * email — send a transactional email via configured SMTP
13
+ * webhook - POST/PUT/GET to an external URL with interpolated body
14
+ * email - send a transactional email via configured SMTP
15
15
  *
16
16
  * Template interpolation in step configs:
17
17
  * {{entry.id}} → source entry id
@@ -23,7 +23,7 @@
23
23
  * {{user.role}} → executing user's role
24
24
  * {{env.CMS_PUBLIC_*}} → environment variables with CMS_PUBLIC_ prefix only
25
25
  *
26
- * Note: execution is not transactional — partial results are returned on failure
26
+ * Note: execution is not transactional - partial results are returned on failure
27
27
  * with a `stepsCompleted` count.
28
28
  */
29
29
  import {v4 as uuidv4} from 'uuid';
@@ -151,16 +151,16 @@ async function executeStep(step, collection, entry, context) {
151
151
  }
152
152
 
153
153
  /*
154
- * createInCollection — append an entry to another collection without
154
+ * createInCollection - append an entry to another collection without
155
155
  * touching the source. Designed for relational patterns like:
156
156
  * - Apply for job → create entry in `applications` { jobId, candidateId, status }
157
157
  * - Bookmark page → create entry in `bookmarks` { pageId, userId }
158
158
  * - Upvote item → create entry in `votes` { itemId, userId, value: 1 }
159
159
  *
160
160
  * Config:
161
- * targetCollection {string} required — slug of the collection to append to
162
- * data {object} required — field map; values are template-interpolated
163
- * createdBy {string} optional — '{{user.id}}' is the conventional value;
161
+ * targetCollection {string} required - slug of the collection to append to
162
+ * data {object} required - field map; values are template-interpolated
163
+ * createdBy {string} optional - '{{user.id}}' is the conventional value;
164
164
  * enables row-level "mine" filtering downstream
165
165
  *
166
166
  * Returns: { created: <new entry id>, in: <targetCollection> }
@@ -303,7 +303,7 @@ export async function createAction(data, userId = null) {
303
303
  confirmMessage: trigger?.confirmMessage || null
304
304
  },
305
305
  steps,
306
- // State-machine transition — when set, the action is gated by the
306
+ // State-machine transition - when set, the action is gated by the
307
307
  // entry's current value of `field` matching one of the `from` values,
308
308
  // and is advertised as an "available transition" for that state.
309
309
  // Optional `to` is informational only (steps still do the actual write).
@@ -399,12 +399,12 @@ export async function listActionsForCollection(collectionSlug) {
399
399
  * 3. action.access.roles allows the user (compared by role level so
400
400
  * higher-privileged roles inherit access automatically)
401
401
  *
402
- * Returns a stripped projection — slug, title, trigger config, transition —
402
+ * Returns a stripped projection - slug, title, trigger config, transition -
403
403
  * so the client can render buttons without leaking step internals.
404
404
  *
405
405
  * @param {string} collectionSlug
406
406
  * @param {object} entry - The entry being inspected (needed for field-value match)
407
- * @param {object} [user] - { role } — anonymous if missing
407
+ * @param {object} [user] - { role } - anonymous if missing
408
408
  * @returns {Promise<object[]>}
409
409
  */
410
410
  export async function listTransitionsForEntry(collectionSlug, entry, user = null) {
@@ -414,7 +414,7 @@ export async function listTransitionsForEntry(collectionSlug, entry, user = null
414
414
  const { getRoleLevel } = await import('./roles.js');
415
415
  const { getEffectiveLevel } = await import('./userRoles.js');
416
416
  // Use effective level so multi-role users see transitions any of their
417
- // roles permit — e.g. a user who's primarily a candidate but also an
417
+ // roles permit - e.g. a user who's primarily a candidate but also an
418
418
  // admin sees BOTH the candidate-only "withdraw" AND the admin-only
419
419
  // "approve" transitions on the same entry.
420
420
  const userLevel = getEffectiveLevel(user);
@@ -446,7 +446,7 @@ export async function listTransitionsForEntry(collectionSlug, entry, user = null
446
446
 
447
447
  /**
448
448
  * Execute an action against a specific collection entry.
449
- * Steps run sequentially — not transactional.
449
+ * Steps run sequentially - not transactional.
450
450
  * Returns partial results with stepsCompleted count on failure.
451
451
  *
452
452
  * @param {string} slug - Action slug
@@ -469,7 +469,7 @@ export async function executeAction(slug, entryId, { user = null } = {}) {
469
469
  throw err;
470
470
  }
471
471
 
472
- // State-machine guard — an action with a transition config only runs when
472
+ // State-machine guard - an action with a transition config only runs when
473
473
  // the entry's current value of `field` is in `from`. Stops illegal moves
474
474
  // like "withdraw" on an already-rejected application; the API returns 409
475
475
  // so the client can present a meaningful "no longer available" message
@@ -480,7 +480,7 @@ export async function executeAction(slug, entryId, { user = null } = {}) {
480
480
  const currentValue = entry.data?.[t.field];
481
481
  const allowed = Array.isArray(t.from) ? t.from : (t.from ? [t.from] : []);
482
482
  if (allowed.length && !allowed.includes(currentValue)) {
483
- const err = new Error(`Cannot apply "${action.title}" — current ${t.field} is "${currentValue ?? '(none)'}", allowed: ${allowed.join(', ')}`);
483
+ const err = new Error(`Cannot apply "${action.title}" - current ${t.field} is "${currentValue ?? '(none)'}", allowed: ${allowed.join(', ')}`);
484
484
  err.statusCode = 409;
485
485
  throw err;
486
486
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * FileAdapter — file-based storage adapter for Collection entries.
2
+ * FileAdapter - file-based storage adapter for Collection entries.
3
3
  *
4
4
  * Stores entries in `data.json` inside the collection directory.
5
5
  * This is the default adapter for all collections and the only adapter
@@ -56,7 +56,7 @@ export class FileAdapter {
56
56
  *
57
57
  * `search` is a simple substring match across all field values (legacy
58
58
  * behaviour, kept for the admin search box). `filter` is the structured
59
- * DSL — see `filterEngine.js` for operator syntax. Both may be combined;
59
+ * DSL - see `filterEngine.js` for operator syntax. Both may be combined;
60
60
  * search runs first, then filter.
61
61
  *
62
62
  * @param {string} slug
@@ -1,10 +1,10 @@
1
1
  /**
2
- * MongoAdapter — MongoDB storage adapter for Collection entries. (Pro feature)
2
+ * MongoAdapter - MongoDB storage adapter for Collection entries. (Pro feature)
3
3
  *
4
4
  * Each CMS collection maps to a MongoDB collection prefixed with `cms_`
5
5
  * (e.g. slug "contacts" → MongoDB collection "cms_contacts").
6
6
  *
7
- * Entry format is preserved: { id, data, meta } — identical to FileAdapter output.
7
+ * Entry format is preserved: { id, data, meta } - identical to FileAdapter output.
8
8
  * MongoDB's _id field is stripped from all returned documents.
9
9
  *
10
10
  * Adapter interface:
@@ -66,7 +66,7 @@ export class MongoAdapter {
66
66
  * and structured filtering.
67
67
  *
68
68
  * `filter` is translated to native MongoDB query operators via
69
- * `filterEngine.toMongoQuery()` and pushed down to the database — page
69
+ * `filterEngine.toMongoQuery()` and pushed down to the database - page
70
70
  * authors writing `[collection where_salary_gte="50000"]` get index-aware
71
71
  * queries automatically. When `search` is present, all documents are
72
72
  * fetched and filtered in memory (same behaviour as FileAdapter, kept
@@ -200,7 +200,7 @@ export class MongoAdapter {
200
200
 
201
201
  /**
202
202
  * Drop the backing MongoDB collection entirely (documents, indexes and
203
- * the namespace). Used when the CMS collection itself is deleted —
203
+ * the namespace). Used when the CMS collection itself is deleted -
204
204
  * clear() only removes documents and would leave `cms_<slug>` behind.
205
205
  *
206
206
  * Deliberately bypasses _col(): that helper ensures the unique index,
@@ -213,7 +213,7 @@ export class MongoAdapter {
213
213
  try {
214
214
  await this._db.collection(`${PREFIX}${slug}`).drop();
215
215
  } catch (err) {
216
- // Already absent — nothing to drop.
216
+ // Already absent - nothing to drop.
217
217
  if (err.codeName !== 'NamespaceNotFound') throw err;
218
218
  }
219
219
  this._ensured.delete(slug);
@@ -5,12 +5,12 @@
5
5
  *
6
6
  * Definitions are DATA, never code: entries in the file-based `api-endpoints`
7
7
  * preset collection. A definition binds a URL path (with `:param` segments)
8
- * to a collection query — fixed filters whose values may carry
8
+ * to a collection query - fixed filters whose values may carry
9
9
  * `{{params.<name>}}` / `{{query.<name>}}` placeholders, sort/limit, a read
10
10
  * field allowlist, and an auth mode (public / token / role) that reuses the
11
11
  * exact machinery of the public collections API.
12
12
  *
13
- * Substituted values only ever feed the filterEngine — there is no eval and
13
+ * Substituted values only ever feed the filterEngine - there is no eval and
14
14
  * no way for a definition to execute code.
15
15
  */
16
16
  import {createEntry, deleteEntry, getCollection, getEntry, listEntries, updateEntry} from './collections.js';
@@ -42,7 +42,7 @@ function invalidateRegistry() {
42
42
  registry = null;
43
43
  }
44
44
 
45
- // Generic admin collection endpoints emit these for ALL collections — the
45
+ // Generic admin collection endpoints emit these for ALL collections - the
46
46
  // service's own mutations invalidate directly, this catches edits made
47
47
  // through the admin entries grid.
48
48
  for (const ev of ['collection:entryCreated', 'collection:entryUpdated', 'collection:entryDeleted']) {
@@ -79,7 +79,7 @@ function sanitise(entry) {
79
79
  }
80
80
 
81
81
  /**
82
- * Normalise a path to its shape for duplicate detection — every `:param`
82
+ * Normalise a path to its shape for duplicate detection - every `:param`
83
83
  * segment collapses to `:` so `/a/:x` and `/a/:y` are the same shape.
84
84
  *
85
85
  * @param {string} path
@@ -113,12 +113,12 @@ async function validateDefinition(data, {excludeId} = {}) {
113
113
  if (schema.systemManaged || schema.preset) {
114
114
  throw new Error(`Collection "${data.collection}" is system-managed and cannot be exposed`);
115
115
  }
116
- // An endpoint may only expose collections from its own project or core —
116
+ // An endpoint may only expose collections from its own project or core -
117
117
  // the URL namespace must match where the data lives, and scoped users
118
118
  // must not be able to publish another project's data through their own.
119
119
  const collectionProject = resolveArtefactProject(schema);
120
120
  if (collectionProject !== data.project && collectionProject !== CORE_PROJECT_SLUG) {
121
- throw new Error(`Collection "${data.collection}" belongs to project "${collectionProject}" — an endpoint may only expose collections from its own project or core`);
121
+ throw new Error(`Collection "${data.collection}" belongs to project "${collectionProject}" - an endpoint may only expose collections from its own project or core`);
122
122
  }
123
123
 
124
124
  if (typeof data.path !== 'string' || !data.path.startsWith('/')) {
@@ -131,13 +131,13 @@ async function validateDefinition(data, {excludeId} = {}) {
131
131
  if (PARAM_SEGMENT_RE.test(seg)) {
132
132
  paramNames.add(seg.slice(1));
133
133
  } else if (!STATIC_SEGMENT_RE.test(seg)) {
134
- throw new Error(`Invalid path segment "${seg}" — use lowercase letters, numbers, hyphens, or :param`);
134
+ throw new Error(`Invalid path segment "${seg}" - use lowercase letters, numbers, hyphens, or :param`);
135
135
  }
136
136
  }
137
137
 
138
138
  const auth = data.auth || 'public';
139
139
  if (auth !== 'public' && auth !== 'token' && !getRoleMap().has(auth)) {
140
- throw new Error(`Unknown auth mode "${auth}" — use public, token, or a role name`);
140
+ throw new Error(`Unknown auth mode "${auth}" - use public, token, or a role name`);
141
141
  }
142
142
 
143
143
  if (data.mode != null && !MODES.includes(data.mode)) throw new Error('mode must be "list" or "single"');
@@ -170,7 +170,7 @@ async function validateDefinition(data, {excludeId} = {}) {
170
170
  }
171
171
  }
172
172
 
173
- // Duplicate shape per project — `/a/:x` and `/a/:y` collide.
173
+ // Duplicate shape per project - `/a/:x` and `/a/:y` collide.
174
174
  const shape = pathShape(data.path);
175
175
  const {entries} = await listEntries(API_ENDPOINTS_COLLECTION_SLUG, {limit: 0});
176
176
  const clash = entries.find(e =>
@@ -216,7 +216,7 @@ export async function createEndpoint(input) {
216
216
 
217
217
  /**
218
218
  * Update an endpoint. The project binding is immutable (it is the endpoint's
219
- * URL namespace) — create a new endpoint to move one.
219
+ * URL namespace) - create a new endpoint to move one.
220
220
  *
221
221
  * @param {string} id
222
222
  * @param {object} patch
@@ -387,14 +387,14 @@ function substituteFilter(filterTemplate, params, query) {
387
387
  }
388
388
 
389
389
  /**
390
- * Query keys that control the query envelope, not a field to filter on — they
390
+ * Query keys that control the query envelope, not a field to filter on - they
391
391
  * must never be interpreted as ad-hoc filters (otherwise `?limit=5` would
392
392
  * filter a field called "limit").
393
393
  */
394
394
  const RESERVED_QUERY_KEYS = new Set(['page', 'limit', 'sort', 'order', 'search']);
395
395
 
396
396
  /**
397
- * The fields an endpoint is allowed to expose — and therefore the only fields
397
+ * The fields an endpoint is allowed to expose - and therefore the only fields
398
398
  * an ad-hoc query filter may touch. A read-field allowlist, when set, is the
399
399
  * boundary; otherwise every declared collection field (plus the always-safe
400
400
  * `id` / `createdBy` pseudo-fields) is fair game. Restricting ad-hoc filters
@@ -415,7 +415,7 @@ async function exposedFieldSet(def) {
415
415
  * Collect ad-hoc filters from the request query, layered UNDER the definition's
416
416
  * declared filters: reserved envelope keys are skipped, values must target an
417
417
  * exposed field, and a field already constrained by a declared filter is left
418
- * to the declaration (the curated filter always wins — a caller cannot loosen
418
+ * to the declaration (the curated filter always wins - a caller cannot loosen
419
419
  * or override it).
420
420
  *
421
421
  * @param {Record<string, unknown>} query
@@ -442,7 +442,7 @@ function collectAdHocFilter(query, exposed, declaredFields) {
442
442
  *
443
443
  * The final filter is the definition's declared filters (fixed literals,
444
444
  * {{params.x}}, {{query.x}}) with ad-hoc query-string filters layered beneath
445
- * them — declared always wins on any overlap.
445
+ * them - declared always wins on any overlap.
446
446
  *
447
447
  * @param {object} def
448
448
  * @param {Record<string, string>} params - Decoded path params
@@ -458,7 +458,7 @@ export async function executeEndpoint(def, params, query) {
458
458
  const adhoc = collectAdHocFilter(q, exposed, declaredFields);
459
459
  const filter = {...adhoc, ...declared}; // declared wins on any key collision
460
460
 
461
- // Envelope controls — the reserved query keys tune pagination and sort
461
+ // Envelope controls - the reserved query keys tune pagination and sort
462
462
  // rather than filter (list mode only). The definition's own limit is the
463
463
  // ceiling: a caller may page/shrink within it but never pull more than the
464
464
  // endpoint intends; sort is restricted to exposed fields.
@@ -3,7 +3,7 @@
3
3
  * Project-scoped machine credentials for the external collections API.
4
4
  *
5
5
  * Tokens are entries in the file-based `api-tokens` preset collection.
6
- * Only a SHA-256 hash is stored — the plaintext (`dcms_<64 hex>`) is returned
6
+ * Only a SHA-256 hash is stored - the plaintext (`dcms_<64 hex>`) is returned
7
7
  * exactly once, from createToken(). A token is accepted only on collection
8
8
  * verbs configured with `api.<verb>.access === 'token'`, and only for
9
9
  * collections whose resolved project matches the token's `project` binding.
@@ -26,7 +26,7 @@ let tokenCache = null;
26
26
  * Serialises this service's fire-and-forget writes (the throttled lastUsedAt
27
27
  * update) against its mutations. FileAdapter does unlocked read-modify-write,
28
28
  * so an in-flight lastUsedAt update that read the data file BEFORE a revoke
29
- * wrote it would write the revoked token straight back — resurrection.
29
+ * wrote it would write the revoked token straight back - resurrection.
30
30
  * Mutations await the chain; lastUsedAt updates append to it.
31
31
  */
32
32
  let writeChain = Promise.resolve();
@@ -35,7 +35,7 @@ function invalidateCache() {
35
35
  tokenCache = null;
36
36
  }
37
37
 
38
- // Generic admin collection endpoints emit these for ALL collections — the
38
+ // Generic admin collection endpoints emit these for ALL collections - the
39
39
  // service's own mutations invalidate directly, this catches edits made
40
40
  // through the admin entries grid.
41
41
  for (const ev of ['collection:entryCreated', 'collection:entryUpdated', 'collection:entryDeleted']) {
@@ -162,7 +162,7 @@ export async function findTokenByName(name, project) {
162
162
  /**
163
163
  * Validate a presented plaintext token.
164
164
  * Returns the sanitised entry when the token exists, is enabled, and has not
165
- * expired — otherwise null. Updates lastUsedAt at most once per minute
165
+ * expired - otherwise null. Updates lastUsedAt at most once per minute
166
166
  * (fire-and-forget; the cached copy is patched in place to avoid thrash).
167
167
  *
168
168
  * @param {string} plaintext
@@ -184,7 +184,7 @@ export async function validateToken(plaintext) {
184
184
 
185
185
  const lastUsed = entry.data.lastUsedAt ? Date.parse(entry.data.lastUsedAt) : 0;
186
186
  if (Date.now() - lastUsed > LAST_USED_THROTTLE_MS) {
187
- // updateEntry replaces data wholesale — pass the full object.
187
+ // updateEntry replaces data wholesale - pass the full object.
188
188
  const data = {...entry.data, lastUsedAt: new Date().toISOString()};
189
189
  entry.data = data; // keep the cached copy current without invalidating
190
190
  // Fire-and-forget, but chained so revoke/update can await it.
@@ -222,7 +222,7 @@ export async function getTokenSanitised(id) {
222
222
 
223
223
  /**
224
224
  * Update mutable token fields. The project binding, hash, and hint are fixed
225
- * for the token's lifetime — revoke and re-issue to rebind.
225
+ * for the token's lifetime - revoke and re-issue to rebind.
226
226
  *
227
227
  * @param {string} id
228
228
  * @param {{name?: string, enabled?: boolean, scopes?: object[], expiresAt?: string|null}} patch
@@ -264,7 +264,7 @@ export async function updateToken(id, patch = {}) {
264
264
  * @returns {Promise<boolean>}
265
265
  */
266
266
  export async function revokeToken(id) {
267
- await writeChain; // drain any in-flight lastUsedAt write — revoke must be final
267
+ await writeChain; // drain any in-flight lastUsedAt write - revoke must be final
268
268
  const entry = await getEntry(API_TOKENS_COLLECTION_SLUG, id);
269
269
  if (!entry) return false;
270
270
  await deleteEntry(API_TOKENS_COLLECTION_SLUG, id);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Blocks Service
3
3
  * CRUD operations and seeding for reusable HTML block templates in content/blocks/.
4
- * Never overwrites existing files during seeding — user customisations are preserved.
4
+ * Never overwrites existing files during seeding - user customisations are preserved.
5
5
  */
6
6
  import fs from 'fs/promises';
7
7
  import path from 'path';
@@ -21,7 +21,7 @@ async function seedBlock(name, content) {
21
21
  const filePath = path.join(BLOCKS_DIR, `${name}.html`);
22
22
  try {
23
23
  await fs.access(filePath);
24
- // File exists — leave it alone
24
+ // File exists - leave it alone
25
25
  } catch {
26
26
  await fs.writeFile(filePath, content.trim() + '\n', 'utf8');
27
27
  }
@@ -34,7 +34,7 @@ async function seedBlock(name, content) {
34
34
  const BLOCKS = {
35
35
 
36
36
  /**
37
- * contact-info — standalone contact details panel for landing pages.
37
+ * contact-info - standalone contact details panel for landing pages.
38
38
  * Fields: heading, intro, phone, phoneHref, email, address, hours,
39
39
  * facebook, facebookLabel, instagram, instagramLabel,
40
40
  * website, websiteLabel, footnote
@@ -99,7 +99,7 @@ const BLOCKS = {
99
99
  `,
100
100
 
101
101
  /**
102
- * contact-card — for the Contacts form
102
+ * contact-card - for the Contacts form
103
103
  * Fields: full_name, phone_number, email_address
104
104
  */
105
105
  'contact-card': `
@@ -120,7 +120,7 @@ const BLOCKS = {
120
120
  `,
121
121
 
122
122
  /**
123
- * enquiry-card — for the Enquiries form
123
+ * enquiry-card - for the Enquiries form
124
124
  * Fields: full_name, email, phone, subject, message
125
125
  */
126
126
  'enquiry-card': `
@@ -143,7 +143,7 @@ const BLOCKS = {
143
143
  `,
144
144
 
145
145
  /**
146
- * feedback-card — for the Feedback form
146
+ * feedback-card - for the Feedback form
147
147
  * Fields: name, email, rating, category, subject, message, recommend
148
148
  */
149
149
  'feedback-card': `
@@ -159,7 +159,7 @@ const BLOCKS = {
159
159
  <div class="card-body">
160
160
  <p style="margin:0 0 1rem;line-height:1.65;">{{message}}</p>
161
161
  <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem;font-size:.8rem;color:var(--dm-text-muted,#888);">
162
- <span>{{name}} &mdash; {{email}}</span>
162
+ <span>{{name}} - {{email}}</span>
163
163
  <span>Would recommend: <strong>{{recommend}}</strong></span>
164
164
  </div>
165
165
  </div>
@@ -167,7 +167,7 @@ const BLOCKS = {
167
167
  `,
168
168
 
169
169
  /**
170
- * note-card — for the Notes form
170
+ * note-card - for the Notes form
171
171
  * Fields: title, content, category, tags
172
172
  */
173
173
  'note-card': `
@@ -187,7 +187,7 @@ const BLOCKS = {
187
187
  `,
188
188
 
189
189
  /**
190
- * todo-item — for the To-Do form
190
+ * todo-item - for the To-Do form
191
191
  * Fields: title, description, status, priority, due_date, assigned_to
192
192
  */
193
193
  'todo-item': `
@@ -238,7 +238,7 @@ function blockMetaPath(name) {
238
238
  return path.join(BLOCKS_DIR, `${name}.meta.json`);
239
239
  }
240
240
 
241
- /** Per-block CSS size cap — matches the 100 KB site-wide cap, scaled down. */
241
+ /** Per-block CSS size cap - matches the 100 KB site-wide cap, scaled down. */
242
242
  const MAX_CSS_SIZE = 50 * 1024;
243
243
 
244
244
  // ---------------------------------------------------------------------------
@@ -300,7 +300,7 @@ export async function getBlock(name) {
300
300
  let css = '';
301
301
  try {
302
302
  css = await fs.readFile(blockCssPath(name), 'utf8');
303
- } catch { /* no CSS file — treat as empty */
303
+ } catch { /* no CSS file - treat as empty */
304
304
  }
305
305
  return {name, content, css, bundled, ...(meta && {meta})};
306
306
  } catch (err) {
@@ -334,7 +334,7 @@ export async function saveBlock(name, content, {bundled, css, meta} = {}) {
334
334
  await fs.mkdir(BLOCKS_DIR, {recursive: true});
335
335
  await fs.writeFile(blockFilePath(name), content, 'utf8');
336
336
  const metaPath = blockMetaPath(name);
337
- // Merge sidecar meta — bundled flag plus any caller-supplied fields (eg meta.project).
337
+ // Merge sidecar meta - bundled flag plus any caller-supplied fields (eg meta.project).
338
338
  const sidecar = {
339
339
  ...(meta && typeof meta === 'object' ? meta : {}),
340
340
  ...(bundled ? {bundled: true} : {})
@@ -345,7 +345,7 @@ export async function saveBlock(name, content, {bundled, css, meta} = {}) {
345
345
  await fs.unlink(metaPath).catch(() => {
346
346
  });
347
347
  }
348
- // CSS is optional — only touch the file when the caller passed the field explicitly.
348
+ // CSS is optional - only touch the file when the caller passed the field explicitly.
349
349
  if (typeof css === 'string') {
350
350
  if (css.trim().length > 0) {
351
351
  await fs.writeFile(blockCssPath(name), css, 'utf8');
@@ -3,7 +3,7 @@ import {LRU} from '../lru.js';
3
3
  /**
4
4
  * In-memory cache driver with TTL and tag invalidation.
5
5
  *
6
- * Each entry is stored as {value, expiresAt, tags}. TTL is lazy — entries
6
+ * Each entry is stored as {value, expiresAt, tags}. TTL is lazy - entries
7
7
  * past their expiresAt are returned as undefined on the next get and dropped.
8
8
  *
9
9
  * Tag index: a Map<tag, Set<key>>. set() with {tags} adds the key to each
@@ -79,14 +79,14 @@ export class MemoryDriver {
79
79
  this._tagIndex.clear();
80
80
  }
81
81
 
82
- /** Current entry count (sync — for the dashboard widget). */
82
+ /** Current entry count (sync - for the dashboard widget). */
83
83
  size() {
84
84
  return this._lru.size;
85
85
  }
86
86
 
87
87
  /**
88
88
  * Snapshot the currently cached entries as `{key, tags, expiresAt}`.
89
- * No values — they can be megabytes of HTML. Uses `entries()` (non-mutating)
89
+ * No values - they can be megabytes of HTML. Uses `entries()` (non-mutating)
90
90
  * so the LRU order is preserved through the iteration.
91
91
  */
92
92
  listEntries() {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Null driver — used when the cache is disabled.
2
+ * Null driver - used when the cache is disabled.
3
3
  * Every read misses; every write is dropped on the floor.
4
4
  */
5
5
  export class NoneDriver {