@geekity/cms 0.3.0 → 0.4.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 (637) hide show
  1. package/README.md +2091 -214
  2. package/admin/components/fields.njk +165 -0
  3. package/admin/layouts/base.njk +1 -1
  4. package/admin/layouts/settings-page.njk +26 -0
  5. package/admin/layouts/shell.njk +17 -3
  6. package/admin/pages/account/forgot.njk +47 -0
  7. package/admin/pages/account/login.njk +26 -0
  8. package/admin/pages/account/reset.njk +38 -0
  9. package/admin/pages/account/setup.njk +28 -0
  10. package/admin/pages/appearance/themes.njk +69 -0
  11. package/admin/pages/comments/all.njk +133 -0
  12. package/admin/{layouts/dashboard.njk → pages/dashboard/home.njk} +8 -15
  13. package/admin/{layouts/document-conflict.njk → pages/documents/conflict.njk} +1 -0
  14. package/admin/pages/documents/editor.njk +196 -0
  15. package/admin/{layouts/document-list.njk → pages/documents/list.njk} +19 -2
  16. package/admin/pages/documents/taxonomy.njk +95 -0
  17. package/admin/pages/federation/followers.njk +238 -0
  18. package/admin/pages/media/library.njk +160 -0
  19. package/admin/pages/messages/all.njk +110 -0
  20. package/admin/{layouts → pages}/placeholder.njk +5 -1
  21. package/admin/pages/settings/discussion.njk +99 -0
  22. package/admin/pages/settings/email.njk +203 -0
  23. package/admin/pages/settings/federation.njk +75 -0
  24. package/admin/pages/settings/general.njk +29 -0
  25. package/admin/pages/settings/permalinks.njk +50 -0
  26. package/admin/pages/settings/reading.njk +72 -0
  27. package/admin/pages/users/edit.njk +239 -0
  28. package/admin/pages/users/list.njk +48 -0
  29. package/admin/pages/users/new.njk +41 -0
  30. package/admin/static/admin.css +498 -3
  31. package/admin/static/copy.js +52 -0
  32. package/admin/static/editor.js +16 -16
  33. package/admin/static/slug.js +61 -0
  34. package/dist/admin/accounts.d.ts +413 -0
  35. package/dist/admin/accounts.d.ts.map +1 -0
  36. package/dist/admin/accounts.js +680 -0
  37. package/dist/admin/accounts.js.map +1 -0
  38. package/dist/admin/appearance.d.ts +80 -0
  39. package/dist/admin/appearance.d.ts.map +1 -0
  40. package/dist/admin/appearance.js +136 -0
  41. package/dist/admin/appearance.js.map +1 -0
  42. package/dist/admin/comments.d.ts +90 -0
  43. package/dist/admin/comments.d.ts.map +1 -0
  44. package/dist/admin/comments.js +244 -0
  45. package/dist/admin/comments.js.map +1 -0
  46. package/dist/admin/credentials.d.ts +26 -2
  47. package/dist/admin/credentials.d.ts.map +1 -1
  48. package/dist/admin/credentials.js +35 -2
  49. package/dist/admin/credentials.js.map +1 -1
  50. package/dist/admin/documents.d.ts +127 -10
  51. package/dist/admin/documents.d.ts.map +1 -1
  52. package/dist/admin/documents.js +360 -41
  53. package/dist/admin/documents.js.map +1 -1
  54. package/dist/admin/federation.d.ts +151 -50
  55. package/dist/admin/federation.d.ts.map +1 -1
  56. package/dist/admin/federation.js +225 -93
  57. package/dist/admin/federation.js.map +1 -1
  58. package/dist/admin/formatting.d.ts +20 -0
  59. package/dist/admin/formatting.d.ts.map +1 -0
  60. package/dist/admin/formatting.js +72 -0
  61. package/dist/admin/formatting.js.map +1 -0
  62. package/dist/admin/headers.d.ts +65 -0
  63. package/dist/admin/headers.d.ts.map +1 -0
  64. package/dist/admin/headers.js +104 -0
  65. package/dist/admin/headers.js.map +1 -0
  66. package/dist/admin/index.d.ts +38 -12
  67. package/dist/admin/index.d.ts.map +1 -1
  68. package/dist/admin/index.js +25 -7
  69. package/dist/admin/index.js.map +1 -1
  70. package/dist/admin/media.d.ts +168 -0
  71. package/dist/admin/media.d.ts.map +1 -0
  72. package/dist/admin/media.js +295 -0
  73. package/dist/admin/media.js.map +1 -0
  74. package/dist/admin/menu.d.ts +79 -0
  75. package/dist/admin/menu.d.ts.map +1 -0
  76. package/dist/admin/menu.js +126 -0
  77. package/dist/admin/menu.js.map +1 -0
  78. package/dist/admin/messages.d.ts +80 -0
  79. package/dist/admin/messages.d.ts.map +1 -0
  80. package/dist/admin/messages.js +152 -0
  81. package/dist/admin/messages.js.map +1 -0
  82. package/dist/admin/preview.d.ts.map +1 -1
  83. package/dist/admin/preview.js +31 -7
  84. package/dist/admin/preview.js.map +1 -1
  85. package/dist/admin/recovery.d.ts +92 -0
  86. package/dist/admin/recovery.d.ts.map +1 -0
  87. package/dist/admin/recovery.js +229 -0
  88. package/dist/admin/recovery.js.map +1 -0
  89. package/dist/admin/routes.d.ts +0 -20
  90. package/dist/admin/routes.d.ts.map +1 -1
  91. package/dist/admin/routes.js +155 -105
  92. package/dist/admin/routes.js.map +1 -1
  93. package/dist/admin/settings-discussion.d.ts +37 -0
  94. package/dist/admin/settings-discussion.d.ts.map +1 -0
  95. package/dist/admin/settings-discussion.js +117 -0
  96. package/dist/admin/settings-discussion.js.map +1 -0
  97. package/dist/admin/settings-email.d.ts +54 -0
  98. package/dist/admin/settings-email.d.ts.map +1 -0
  99. package/dist/admin/settings-email.js +189 -0
  100. package/dist/admin/settings-email.js.map +1 -0
  101. package/dist/admin/settings-federation.d.ts +39 -0
  102. package/dist/admin/settings-federation.d.ts.map +1 -0
  103. package/dist/admin/settings-federation.js +103 -0
  104. package/dist/admin/settings-federation.js.map +1 -0
  105. package/dist/admin/settings-general.d.ts +12 -0
  106. package/dist/admin/settings-general.d.ts.map +1 -0
  107. package/dist/admin/settings-general.js +48 -0
  108. package/dist/admin/settings-general.js.map +1 -0
  109. package/dist/admin/settings-page.d.ts +102 -0
  110. package/dist/admin/settings-page.d.ts.map +1 -0
  111. package/dist/admin/settings-page.js +126 -0
  112. package/dist/admin/settings-page.js.map +1 -0
  113. package/dist/admin/settings-pages.d.ts +19 -0
  114. package/dist/admin/settings-pages.d.ts.map +1 -0
  115. package/dist/admin/settings-pages.js +33 -0
  116. package/dist/admin/settings-pages.js.map +1 -0
  117. package/dist/admin/settings-permalinks.d.ts +16 -0
  118. package/dist/admin/settings-permalinks.d.ts.map +1 -0
  119. package/dist/admin/settings-permalinks.js +32 -0
  120. package/dist/admin/settings-permalinks.js.map +1 -0
  121. package/dist/admin/settings-reading.d.ts +33 -0
  122. package/dist/admin/settings-reading.d.ts.map +1 -0
  123. package/dist/admin/settings-reading.js +48 -0
  124. package/dist/admin/settings-reading.js.map +1 -0
  125. package/dist/admin/settings.d.ts +325 -96
  126. package/dist/admin/settings.d.ts.map +1 -1
  127. package/dist/admin/settings.js +650 -308
  128. package/dist/admin/settings.js.map +1 -1
  129. package/dist/admin/store.d.ts +606 -152
  130. package/dist/admin/store.d.ts.map +1 -1
  131. package/dist/admin/store.js +863 -268
  132. package/dist/admin/store.js.map +1 -1
  133. package/dist/admin/taxonomy.d.ts +114 -0
  134. package/dist/admin/taxonomy.d.ts.map +1 -0
  135. package/dist/admin/taxonomy.js +289 -0
  136. package/dist/admin/taxonomy.js.map +1 -0
  137. package/dist/admin/templates.d.ts +44 -14
  138. package/dist/admin/templates.d.ts.map +1 -1
  139. package/dist/admin/templates.js +44 -14
  140. package/dist/admin/templates.js.map +1 -1
  141. package/dist/admin/throttle.d.ts +84 -0
  142. package/dist/admin/throttle.d.ts.map +1 -0
  143. package/dist/admin/throttle.js +143 -0
  144. package/dist/admin/throttle.js.map +1 -0
  145. package/dist/admin/uploads.d.ts +28 -2
  146. package/dist/admin/uploads.d.ts.map +1 -1
  147. package/dist/admin/uploads.js +43 -4
  148. package/dist/admin/uploads.js.map +1 -1
  149. package/dist/admin/users.d.ts +100 -13
  150. package/dist/admin/users.d.ts.map +1 -1
  151. package/dist/admin/users.js +412 -39
  152. package/dist/admin/users.js.map +1 -1
  153. package/dist/cache.d.ts +141 -0
  154. package/dist/cache.d.ts.map +1 -0
  155. package/dist/cache.js +207 -0
  156. package/dist/cache.js.map +1 -0
  157. package/dist/cli.d.ts +14 -2
  158. package/dist/cli.d.ts.map +1 -1
  159. package/dist/cli.js +424 -21
  160. package/dist/cli.js.map +1 -1
  161. package/dist/comments/akismet.d.ts +130 -0
  162. package/dist/comments/akismet.d.ts.map +1 -0
  163. package/dist/comments/akismet.js +274 -0
  164. package/dist/comments/akismet.js.map +1 -0
  165. package/dist/comments/conversation.d.ts +29 -0
  166. package/dist/comments/conversation.d.ts.map +1 -0
  167. package/dist/comments/conversation.js +60 -0
  168. package/dist/comments/conversation.js.map +1 -0
  169. package/dist/comments/form.d.ts +75 -0
  170. package/dist/comments/form.d.ts.map +1 -0
  171. package/dist/comments/form.js +64 -0
  172. package/dist/comments/form.js.map +1 -0
  173. package/dist/comments/index.d.ts +15 -0
  174. package/dist/comments/index.d.ts.map +1 -0
  175. package/dist/comments/index.js +9 -0
  176. package/dist/comments/index.js.map +1 -0
  177. package/dist/comments/markdown.d.ts +8 -0
  178. package/dist/comments/markdown.d.ts.map +1 -0
  179. package/dist/comments/markdown.js +85 -0
  180. package/dist/comments/markdown.js.map +1 -0
  181. package/dist/comments/moderate.d.ts +71 -0
  182. package/dist/comments/moderate.d.ts.map +1 -0
  183. package/dist/comments/moderate.js +88 -0
  184. package/dist/comments/moderate.js.map +1 -0
  185. package/dist/comments/policy.d.ts +57 -0
  186. package/dist/comments/policy.d.ts.map +1 -0
  187. package/dist/comments/policy.js +80 -0
  188. package/dist/comments/policy.js.map +1 -0
  189. package/dist/comments/records.d.ts +266 -0
  190. package/dist/comments/records.d.ts.map +1 -0
  191. package/dist/comments/records.js +487 -0
  192. package/dist/comments/records.js.map +1 -0
  193. package/dist/comments/routes.d.ts +58 -0
  194. package/dist/comments/routes.d.ts.map +1 -0
  195. package/dist/comments/routes.js +177 -0
  196. package/dist/comments/routes.js.map +1 -0
  197. package/dist/comments/submission.d.ts +263 -0
  198. package/dist/comments/submission.d.ts.map +1 -0
  199. package/dist/comments/submission.js +241 -0
  200. package/dist/comments/submission.js.map +1 -0
  201. package/dist/config.d.ts +190 -5
  202. package/dist/config.d.ts.map +1 -1
  203. package/dist/config.js +118 -6
  204. package/dist/config.js.map +1 -1
  205. package/dist/contact/delivery.d.ts +60 -0
  206. package/dist/contact/delivery.d.ts.map +1 -0
  207. package/dist/contact/delivery.js +87 -0
  208. package/dist/contact/delivery.js.map +1 -0
  209. package/dist/contact/form.d.ts +130 -0
  210. package/dist/contact/form.d.ts.map +1 -0
  211. package/dist/contact/form.js +163 -0
  212. package/dist/contact/form.js.map +1 -0
  213. package/dist/contact/index.d.ts +17 -0
  214. package/dist/contact/index.d.ts.map +1 -0
  215. package/dist/contact/index.js +13 -0
  216. package/dist/contact/index.js.map +1 -0
  217. package/dist/contact/records.d.ts +124 -0
  218. package/dist/contact/records.d.ts.map +1 -0
  219. package/dist/contact/records.js +218 -0
  220. package/dist/contact/records.js.map +1 -0
  221. package/dist/contact/routes.d.ts +17 -0
  222. package/dist/contact/routes.d.ts.map +1 -0
  223. package/dist/contact/routes.js +127 -0
  224. package/dist/contact/routes.js.map +1 -0
  225. package/dist/contact/submission.d.ts +103 -0
  226. package/dist/contact/submission.d.ts.map +1 -0
  227. package/dist/contact/submission.js +166 -0
  228. package/dist/contact/submission.js.map +1 -0
  229. package/dist/content/document.d.ts +7 -2
  230. package/dist/content/document.d.ts.map +1 -1
  231. package/dist/content/document.js +1 -0
  232. package/dist/content/document.js.map +1 -1
  233. package/dist/content/index.d.ts +7 -2
  234. package/dist/content/index.d.ts.map +1 -1
  235. package/dist/content/index.js +4 -1
  236. package/dist/content/index.js.map +1 -1
  237. package/dist/content/markdown.d.ts.map +1 -1
  238. package/dist/content/markdown.js +24 -2
  239. package/dist/content/markdown.js.map +1 -1
  240. package/dist/content/parser.d.ts.map +1 -1
  241. package/dist/content/parser.js +11 -4
  242. package/dist/content/parser.js.map +1 -1
  243. package/dist/content/save.d.ts +5 -0
  244. package/dist/content/save.d.ts.map +1 -1
  245. package/dist/content/save.js +44 -1
  246. package/dist/content/save.js.map +1 -1
  247. package/dist/content/schedule.d.ts +125 -0
  248. package/dist/content/schedule.d.ts.map +1 -0
  249. package/dist/content/schedule.js +207 -0
  250. package/dist/content/schedule.js.map +1 -0
  251. package/dist/content/search.d.ts +64 -0
  252. package/dist/content/search.d.ts.map +1 -0
  253. package/dist/content/search.js +136 -0
  254. package/dist/content/search.js.map +1 -0
  255. package/dist/content/store.d.ts +202 -7
  256. package/dist/content/store.d.ts.map +1 -1
  257. package/dist/content/store.js +374 -67
  258. package/dist/content/store.js.map +1 -1
  259. package/dist/content/sync.d.ts +7 -1
  260. package/dist/content/sync.d.ts.map +1 -1
  261. package/dist/content/sync.js +18 -10
  262. package/dist/content/sync.js.map +1 -1
  263. package/dist/content/time.d.ts +56 -0
  264. package/dist/content/time.d.ts.map +1 -0
  265. package/dist/content/time.js +141 -0
  266. package/dist/content/time.js.map +1 -0
  267. package/dist/content/writer.d.ts.map +1 -1
  268. package/dist/content/writer.js +3 -0
  269. package/dist/content/writer.js.map +1 -1
  270. package/dist/env.d.ts +58 -4
  271. package/dist/env.d.ts.map +1 -1
  272. package/dist/federation/actor.d.ts +95 -41
  273. package/dist/federation/actor.d.ts.map +1 -1
  274. package/dist/federation/actor.js +180 -53
  275. package/dist/federation/actor.js.map +1 -1
  276. package/dist/federation/article.d.ts +44 -17
  277. package/dist/federation/article.d.ts.map +1 -1
  278. package/dist/federation/article.js +98 -46
  279. package/dist/federation/article.js.map +1 -1
  280. package/dist/federation/delivery.d.ts +63 -15
  281. package/dist/federation/delivery.d.ts.map +1 -1
  282. package/dist/federation/delivery.js +154 -80
  283. package/dist/federation/delivery.js.map +1 -1
  284. package/dist/federation/federation.d.ts +22 -17
  285. package/dist/federation/federation.d.ts.map +1 -1
  286. package/dist/federation/federation.js +98 -77
  287. package/dist/federation/federation.js.map +1 -1
  288. package/dist/federation/followers.d.ts +3 -2
  289. package/dist/federation/followers.d.ts.map +1 -1
  290. package/dist/federation/followers.js +6 -5
  291. package/dist/federation/followers.js.map +1 -1
  292. package/dist/federation/import-wordpress.d.ts +128 -0
  293. package/dist/federation/import-wordpress.d.ts.map +1 -0
  294. package/dist/federation/import-wordpress.js +426 -0
  295. package/dist/federation/import-wordpress.js.map +1 -0
  296. package/dist/federation/inbox.d.ts +36 -13
  297. package/dist/federation/inbox.d.ts.map +1 -1
  298. package/dist/federation/inbox.js +120 -49
  299. package/dist/federation/inbox.js.map +1 -1
  300. package/dist/federation/index.d.ts +22 -10
  301. package/dist/federation/index.d.ts.map +1 -1
  302. package/dist/federation/index.js +13 -8
  303. package/dist/federation/index.js.map +1 -1
  304. package/dist/federation/keys.d.ts +98 -20
  305. package/dist/federation/keys.d.ts.map +1 -1
  306. package/dist/federation/keys.js +269 -39
  307. package/dist/federation/keys.js.map +1 -1
  308. package/dist/federation/mount.d.ts +42 -10
  309. package/dist/federation/mount.d.ts.map +1 -1
  310. package/dist/federation/mount.js +266 -26
  311. package/dist/federation/mount.js.map +1 -1
  312. package/dist/federation/paths.d.ts +23 -35
  313. package/dist/federation/paths.d.ts.map +1 -1
  314. package/dist/federation/paths.js +26 -39
  315. package/dist/federation/paths.js.map +1 -1
  316. package/dist/federation/records.d.ts +223 -0
  317. package/dist/federation/records.d.ts.map +1 -0
  318. package/dist/federation/records.js +506 -0
  319. package/dist/federation/records.js.map +1 -0
  320. package/dist/federation/relays.d.ts +114 -0
  321. package/dist/federation/relays.d.ts.map +1 -0
  322. package/dist/federation/relays.js +290 -0
  323. package/dist/federation/relays.js.map +1 -0
  324. package/dist/federation/replies.d.ts +80 -0
  325. package/dist/federation/replies.d.ts.map +1 -0
  326. package/dist/federation/replies.js +166 -0
  327. package/dist/federation/replies.js.map +1 -0
  328. package/dist/federation/wordpress.d.ts +131 -0
  329. package/dist/federation/wordpress.d.ts.map +1 -0
  330. package/dist/federation/wordpress.js +341 -0
  331. package/dist/federation/wordpress.js.map +1 -0
  332. package/dist/files/atomic.d.ts +103 -0
  333. package/dist/files/atomic.d.ts.map +1 -0
  334. package/dist/files/atomic.js +150 -0
  335. package/dist/files/atomic.js.map +1 -0
  336. package/dist/files/index.d.ts +3 -0
  337. package/dist/files/index.d.ts.map +1 -0
  338. package/dist/files/index.js +2 -0
  339. package/dist/files/index.js.map +1 -0
  340. package/dist/forms/protection.d.ts +85 -0
  341. package/dist/forms/protection.d.ts.map +1 -0
  342. package/dist/forms/protection.js +116 -0
  343. package/dist/forms/protection.js.map +1 -0
  344. package/dist/images/icons.d.ts +59 -0
  345. package/dist/images/icons.d.ts.map +1 -0
  346. package/dist/images/icons.js +131 -0
  347. package/dist/images/icons.js.map +1 -0
  348. package/dist/images/index.d.ts +7 -0
  349. package/dist/images/index.d.ts.map +1 -0
  350. package/dist/images/index.js +4 -0
  351. package/dist/images/index.js.map +1 -0
  352. package/dist/images/markup.d.ts +47 -0
  353. package/dist/images/markup.d.ts.map +1 -0
  354. package/dist/images/markup.js +175 -0
  355. package/dist/images/markup.js.map +1 -0
  356. package/dist/images/paths.d.ts +34 -0
  357. package/dist/images/paths.d.ts.map +1 -0
  358. package/dist/images/paths.js +41 -0
  359. package/dist/images/paths.js.map +1 -0
  360. package/dist/images/variants.d.ts +104 -0
  361. package/dist/images/variants.d.ts.map +1 -0
  362. package/dist/images/variants.js +320 -0
  363. package/dist/images/variants.js.map +1 -0
  364. package/dist/index.d.ts +123 -19
  365. package/dist/index.d.ts.map +1 -1
  366. package/dist/index.js +327 -25
  367. package/dist/index.js.map +1 -1
  368. package/dist/init.d.ts +18 -0
  369. package/dist/init.d.ts.map +1 -1
  370. package/dist/init.js +34 -3
  371. package/dist/init.js.map +1 -1
  372. package/dist/mail/brevo.d.ts +35 -0
  373. package/dist/mail/brevo.d.ts.map +1 -0
  374. package/dist/mail/brevo.js +85 -0
  375. package/dist/mail/brevo.js.map +1 -0
  376. package/dist/mail/credentials.d.ts +62 -0
  377. package/dist/mail/credentials.d.ts.map +1 -0
  378. package/dist/mail/credentials.js +86 -0
  379. package/dist/mail/credentials.js.map +1 -0
  380. package/dist/mail/index.d.ts +15 -0
  381. package/dist/mail/index.d.ts.map +1 -0
  382. package/dist/mail/index.js +8 -0
  383. package/dist/mail/index.js.map +1 -0
  384. package/dist/mail/memory.d.ts +35 -0
  385. package/dist/mail/memory.d.ts.map +1 -0
  386. package/dist/mail/memory.js +29 -0
  387. package/dist/mail/memory.js.map +1 -0
  388. package/dist/mail/provider.d.ts +80 -0
  389. package/dist/mail/provider.d.ts.map +1 -0
  390. package/dist/mail/provider.js +23 -0
  391. package/dist/mail/provider.js.map +1 -0
  392. package/dist/mail/service.d.ts +156 -0
  393. package/dist/mail/service.d.ts.map +1 -0
  394. package/dist/mail/service.js +265 -0
  395. package/dist/mail/service.js.map +1 -0
  396. package/dist/mail/smtp.d.ts +39 -0
  397. package/dist/mail/smtp.d.ts.map +1 -0
  398. package/dist/mail/smtp.js +58 -0
  399. package/dist/mail/smtp.js.map +1 -0
  400. package/dist/mail/templates.d.ts +91 -0
  401. package/dist/mail/templates.d.ts.map +1 -0
  402. package/dist/mail/templates.js +69 -0
  403. package/dist/mail/templates.js.map +1 -0
  404. package/dist/notifications/comments.d.ts +66 -0
  405. package/dist/notifications/comments.d.ts.map +1 -0
  406. package/dist/notifications/comments.js +151 -0
  407. package/dist/notifications/comments.js.map +1 -0
  408. package/dist/notifications/digest.d.ts +135 -0
  409. package/dist/notifications/digest.d.ts.map +1 -0
  410. package/dist/notifications/digest.js +287 -0
  411. package/dist/notifications/digest.js.map +1 -0
  412. package/dist/notifications/index.d.ts +13 -0
  413. package/dist/notifications/index.d.ts.map +1 -0
  414. package/dist/notifications/index.js +8 -0
  415. package/dist/notifications/index.js.map +1 -0
  416. package/dist/notifications/links.d.ts +60 -0
  417. package/dist/notifications/links.d.ts.map +1 -0
  418. package/dist/notifications/links.js +63 -0
  419. package/dist/notifications/links.js.map +1 -0
  420. package/dist/notifications/optouts.d.ts +48 -0
  421. package/dist/notifications/optouts.d.ts.map +1 -0
  422. package/dist/notifications/optouts.js +93 -0
  423. package/dist/notifications/optouts.js.map +1 -0
  424. package/dist/notifications/preferences.d.ts +158 -0
  425. package/dist/notifications/preferences.d.ts.map +1 -0
  426. package/dist/notifications/preferences.js +195 -0
  427. package/dist/notifications/preferences.js.map +1 -0
  428. package/dist/notifications/routes.d.ts +28 -0
  429. package/dist/notifications/routes.d.ts.map +1 -0
  430. package/dist/notifications/routes.js +184 -0
  431. package/dist/notifications/routes.js.map +1 -0
  432. package/dist/notifications/tokens.d.ts +60 -0
  433. package/dist/notifications/tokens.d.ts.map +1 -0
  434. package/dist/notifications/tokens.js +125 -0
  435. package/dist/notifications/tokens.js.map +1 -0
  436. package/dist/notify.d.ts +84 -0
  437. package/dist/notify.d.ts.map +1 -0
  438. package/dist/notify.js +149 -0
  439. package/dist/notify.js.map +1 -0
  440. package/dist/web/archive.d.ts +49 -0
  441. package/dist/web/archive.d.ts.map +1 -0
  442. package/dist/web/archive.js +59 -0
  443. package/dist/web/archive.js.map +1 -0
  444. package/dist/web/assets.d.ts +6 -5
  445. package/dist/web/assets.d.ts.map +1 -1
  446. package/dist/web/assets.js +5 -7
  447. package/dist/web/assets.js.map +1 -1
  448. package/dist/web/authors.d.ts +143 -0
  449. package/dist/web/authors.d.ts.map +1 -0
  450. package/dist/web/authors.js +178 -0
  451. package/dist/web/authors.js.map +1 -0
  452. package/dist/web/comments.d.ts +60 -0
  453. package/dist/web/comments.d.ts.map +1 -0
  454. package/dist/web/comments.js +207 -0
  455. package/dist/web/comments.js.map +1 -0
  456. package/dist/web/context.d.ts +162 -28
  457. package/dist/web/context.d.ts.map +1 -1
  458. package/dist/web/context.js +123 -26
  459. package/dist/web/context.js.map +1 -1
  460. package/dist/web/conversation.d.ts +209 -0
  461. package/dist/web/conversation.d.ts.map +1 -0
  462. package/dist/web/conversation.js +557 -0
  463. package/dist/web/conversation.js.map +1 -0
  464. package/dist/web/documents.d.ts +36 -6
  465. package/dist/web/documents.d.ts.map +1 -1
  466. package/dist/web/documents.js +52 -10
  467. package/dist/web/documents.js.map +1 -1
  468. package/dist/web/feed-atom.d.ts +22 -0
  469. package/dist/web/feed-atom.d.ts.map +1 -0
  470. package/dist/web/feed-atom.js +83 -0
  471. package/dist/web/feed-atom.js.map +1 -0
  472. package/dist/web/feed-item.d.ts +145 -0
  473. package/dist/web/feed-item.d.ts.map +1 -0
  474. package/dist/web/feed-item.js +120 -0
  475. package/dist/web/feed-item.js.map +1 -0
  476. package/dist/web/feed-json.d.ts +79 -0
  477. package/dist/web/feed-json.d.ts.map +1 -0
  478. package/dist/web/feed-json.js +53 -0
  479. package/dist/web/feed-json.js.map +1 -0
  480. package/dist/web/feed-rss.d.ts +26 -0
  481. package/dist/web/feed-rss.d.ts.map +1 -0
  482. package/dist/web/feed-rss.js +203 -0
  483. package/dist/web/feed-rss.js.map +1 -0
  484. package/dist/web/feed-source.d.ts +244 -0
  485. package/dist/web/feed-source.d.ts.map +1 -0
  486. package/dist/web/feed-source.js +201 -0
  487. package/dist/web/feed-source.js.map +1 -0
  488. package/dist/web/feed-xml.d.ts +63 -0
  489. package/dist/web/feed-xml.d.ts.map +1 -0
  490. package/dist/web/feed-xml.js +116 -0
  491. package/dist/web/feed-xml.js.map +1 -0
  492. package/dist/web/feeds.d.ts +31 -96
  493. package/dist/web/feeds.d.ts.map +1 -1
  494. package/dist/web/feeds.js +94 -198
  495. package/dist/web/feeds.js.map +1 -1
  496. package/dist/web/health.d.ts +31 -0
  497. package/dist/web/health.d.ts.map +1 -0
  498. package/dist/web/health.js +44 -0
  499. package/dist/web/health.js.map +1 -0
  500. package/dist/web/index.d.ts +30 -10
  501. package/dist/web/index.d.ts.map +1 -1
  502. package/dist/web/index.js +18 -7
  503. package/dist/web/index.js.map +1 -1
  504. package/dist/web/navigation.d.ts +73 -0
  505. package/dist/web/navigation.d.ts.map +1 -0
  506. package/dist/web/navigation.js +110 -0
  507. package/dist/web/navigation.js.map +1 -0
  508. package/dist/web/negotiate.d.ts +9 -0
  509. package/dist/web/negotiate.d.ts.map +1 -1
  510. package/dist/web/negotiate.js +6 -1
  511. package/dist/web/negotiate.js.map +1 -1
  512. package/dist/web/recent.d.ts +43 -0
  513. package/dist/web/recent.d.ts.map +1 -0
  514. package/dist/web/recent.js +37 -0
  515. package/dist/web/recent.js.map +1 -0
  516. package/dist/web/render.d.ts +197 -9
  517. package/dist/web/render.d.ts.map +1 -1
  518. package/dist/web/render.js +334 -17
  519. package/dist/web/render.js.map +1 -1
  520. package/dist/web/routes.d.ts +25 -8
  521. package/dist/web/routes.d.ts.map +1 -1
  522. package/dist/web/routes.js +845 -93
  523. package/dist/web/routes.js.map +1 -1
  524. package/dist/web/sanitize.d.ts +19 -0
  525. package/dist/web/sanitize.d.ts.map +1 -0
  526. package/dist/web/sanitize.js +272 -0
  527. package/dist/web/sanitize.js.map +1 -0
  528. package/dist/web/search.d.ts +79 -0
  529. package/dist/web/search.d.ts.map +1 -0
  530. package/dist/web/search.js +94 -0
  531. package/dist/web/search.js.map +1 -0
  532. package/dist/web/sitemap.d.ts +120 -0
  533. package/dist/web/sitemap.d.ts.map +1 -0
  534. package/dist/web/sitemap.js +199 -0
  535. package/dist/web/sitemap.js.map +1 -0
  536. package/dist/web/taxonomy.d.ts +152 -0
  537. package/dist/web/taxonomy.d.ts.map +1 -0
  538. package/dist/web/taxonomy.js +237 -0
  539. package/dist/web/taxonomy.js.map +1 -0
  540. package/dist/web/templates.d.ts +54 -16
  541. package/dist/web/templates.d.ts.map +1 -1
  542. package/dist/web/templates.js +109 -28
  543. package/dist/web/templates.js.map +1 -1
  544. package/dist/web/themes.d.ts +205 -0
  545. package/dist/web/themes.d.ts.map +1 -0
  546. package/dist/web/themes.js +277 -0
  547. package/dist/web/themes.js.map +1 -0
  548. package/dist/webmention/discovery.d.ts +51 -0
  549. package/dist/webmention/discovery.d.ts.map +1 -0
  550. package/dist/webmention/discovery.js +176 -0
  551. package/dist/webmention/discovery.js.map +1 -0
  552. package/dist/webmention/html.d.ts +71 -0
  553. package/dist/webmention/html.d.ts.map +1 -0
  554. package/dist/webmention/html.js +285 -0
  555. package/dist/webmention/html.js.map +1 -0
  556. package/dist/webmention/index.d.ts +12 -0
  557. package/dist/webmention/index.d.ts.map +1 -0
  558. package/dist/webmention/index.js +8 -0
  559. package/dist/webmention/index.js.map +1 -0
  560. package/dist/webmention/links.d.ts +18 -0
  561. package/dist/webmention/links.d.ts.map +1 -0
  562. package/dist/webmention/links.js +64 -0
  563. package/dist/webmention/links.js.map +1 -0
  564. package/dist/webmention/microformats.d.ts +93 -0
  565. package/dist/webmention/microformats.d.ts.map +1 -0
  566. package/dist/webmention/microformats.js +376 -0
  567. package/dist/webmention/microformats.js.map +1 -0
  568. package/dist/webmention/receive.d.ts +141 -0
  569. package/dist/webmention/receive.d.ts.map +1 -0
  570. package/dist/webmention/receive.js +264 -0
  571. package/dist/webmention/receive.js.map +1 -0
  572. package/dist/webmention/routes.d.ts +37 -0
  573. package/dist/webmention/routes.d.ts.map +1 -0
  574. package/dist/webmention/routes.js +75 -0
  575. package/dist/webmention/routes.js.map +1 -0
  576. package/dist/webmention/service.d.ts +94 -0
  577. package/dist/webmention/service.d.ts.map +1 -0
  578. package/dist/webmention/service.js +207 -0
  579. package/dist/webmention/service.js.map +1 -0
  580. package/package.json +11 -6
  581. package/templates/site/content/_data/site.json +1 -0
  582. package/templates/site/content/posts/2026-01-01-hello-world.md +5 -3
  583. package/templates/site/geekity.config.ts +11 -1
  584. package/templates/site/tsconfig.json +1 -1
  585. package/themes/default/README.md +1096 -50
  586. package/themes/default/layouts/404.njk +2 -3
  587. package/themes/default/layouts/author.njk +37 -0
  588. package/themes/default/layouts/base.njk +172 -31
  589. package/themes/default/layouts/category.njk +27 -0
  590. package/themes/default/layouts/front-page.njk +57 -0
  591. package/themes/default/layouts/home.njk +17 -1
  592. package/themes/default/layouts/page.njk +57 -5
  593. package/themes/default/layouts/post.njk +78 -19
  594. package/themes/default/layouts/search.njk +57 -0
  595. package/themes/default/layouts/tag.njk +3 -6
  596. package/themes/default/mail/comment-digest.html.njk +37 -0
  597. package/themes/default/mail/comment-digest.subject.njk +3 -0
  598. package/themes/default/mail/comment-digest.txt.njk +40 -0
  599. package/themes/default/mail/comment-pending.html.njk +27 -0
  600. package/themes/default/mail/comment-pending.subject.njk +2 -0
  601. package/themes/default/mail/comment-pending.txt.njk +24 -0
  602. package/themes/default/mail/comment-reply.html.njk +21 -0
  603. package/themes/default/mail/comment-reply.subject.njk +2 -0
  604. package/themes/default/mail/comment-reply.txt.njk +22 -0
  605. package/themes/default/mail/contact-message.html.njk +24 -0
  606. package/themes/default/mail/contact-message.subject.njk +5 -0
  607. package/themes/default/mail/contact-message.txt.njk +20 -0
  608. package/themes/default/mail/password-changed.html.njk +18 -0
  609. package/themes/default/mail/password-changed.subject.njk +2 -0
  610. package/themes/default/mail/password-changed.txt.njk +17 -0
  611. package/themes/default/mail/password-reset.html.njk +22 -0
  612. package/themes/default/mail/password-reset.subject.njk +2 -0
  613. package/themes/default/mail/password-reset.txt.njk +18 -0
  614. package/themes/default/mail/test.html.njk +14 -0
  615. package/themes/default/mail/test.subject.njk +2 -0
  616. package/themes/default/mail/test.txt.njk +12 -0
  617. package/themes/default/partials/archive.njk +27 -0
  618. package/themes/default/partials/bio.njk +77 -0
  619. package/themes/default/partials/comment-form.njk +109 -0
  620. package/themes/default/partials/contact-form.njk +88 -0
  621. package/themes/default/partials/conversation.njk +139 -0
  622. package/themes/default/partials/feeds.njk +26 -0
  623. package/themes/default/partials/jsonld.njk +120 -0
  624. package/themes/default/partials/pagination.njk +12 -6
  625. package/themes/default/partials/post-list.njk +51 -18
  626. package/themes/default/partials/search-form.njk +17 -0
  627. package/themes/default/partials/tags.njk +26 -5
  628. package/themes/default/static/highlight.js +4 -0
  629. package/themes/default/static/style.css +1119 -194
  630. package/themes/default/theme.json +5 -0
  631. package/admin/layouts/document-editor.njk +0 -167
  632. package/admin/layouts/federation.njk +0 -145
  633. package/admin/layouts/login.njk +0 -23
  634. package/admin/layouts/settings.njk +0 -116
  635. package/admin/layouts/setup.njk +0 -27
  636. package/admin/layouts/users.njk +0 -91
  637. /package/admin/{layouts/_flash.njk → components/flash.njk} +0 -0
@@ -0,0 +1,152 @@
1
+ import { countContactMessagesByStatus, countUnreadContactMessages, deleteContactMessage, listContactMessages, setContactMessageRead, } from '../contact/records.js';
2
+ import { flash } from './flash.js';
3
+ import { formatInTimezone } from './formatting.js';
4
+ import { ADMIN_PREFIX } from './session.js';
5
+ import { readSiteSettings } from './settings.js';
6
+ import { ADMIN_TEMPLATES } from './templates.js';
7
+ /**
8
+ * The inbox: what the contact form left behind (TASK-56).
9
+ *
10
+ * A message is a file under `data/contact/` and nothing else — there is no
11
+ * index, because this screen reads every file to sort them anyway and a second
12
+ * copy of the truth would only be a second thing to keep true. So the two
13
+ * actions here are a rewrite and an unlink, and the screen is a view of the
14
+ * directory as it is at that moment (decision-9).
15
+ *
16
+ * Two lists rather than one. A checker's `spam` is an opinion about a message
17
+ * somebody may really have sent, so it is kept rather than thrown away — but a
18
+ * run of it must not bury the messages a person actually has to answer.
19
+ */
20
+ /** Where the Messages screen lives. */
21
+ export const MESSAGES_PATH = `${ADMIN_PREFIX}/messages`;
22
+ /** The navigation section it marks as current. */
23
+ export const MESSAGES_SECTION = 'messages';
24
+ /** Where the Mark read and Mark unread buttons post. */
25
+ export const MESSAGES_READ_PATH = `${MESSAGES_PATH}/read`;
26
+ /** Where the Delete button posts. */
27
+ export const MESSAGES_DELETE_PATH = `${MESSAGES_PATH}/delete`;
28
+ /** The fields the forms on this screen submit. */
29
+ export const MESSAGE_FIELDS = {
30
+ /** Which message a button is about. */
31
+ id: 'id',
32
+ /** Whether Mark read means read or unread. Empty is unread. */
33
+ read: 'read',
34
+ /** Which list to go back to afterwards. */
35
+ status: 'status',
36
+ };
37
+ /** How many messages one page of a list shows. */
38
+ export const MESSAGES_PER_PAGE = 25;
39
+ /** The two lists, in the order the screen offers them. */
40
+ export const MESSAGE_TABS = [
41
+ { status: 'received', label: 'Inbox' },
42
+ { status: 'spam', label: 'Spam' },
43
+ ];
44
+ /** Register the Messages screen. */
45
+ export function mountMessagesScreen(app, options) {
46
+ const { render } = options;
47
+ app.get(MESSAGES_PATH, (c) => {
48
+ const { dataDir, contentDir } = c.var.config;
49
+ const status = statusOf(c.req.query('status'));
50
+ const page = Math.max(1, Math.trunc(Number(c.req.query('page') ?? '1')) || 1);
51
+ const counts = countContactMessagesByStatus(dataDir);
52
+ const timezone = readSiteSettings(contentDir).timezone;
53
+ const rows = listContactMessages(dataDir, { status })
54
+ .slice((page - 1) * MESSAGES_PER_PAGE, page * MESSAGES_PER_PAGE)
55
+ .map((message) => messageRow(message, timezone));
56
+ const pages = Math.max(1, Math.ceil(counts[status] / MESSAGES_PER_PAGE));
57
+ return render(c, ADMIN_TEMPLATES.messages, {
58
+ section: MESSAGES_SECTION,
59
+ child: 'all',
60
+ heading: 'Messages',
61
+ status,
62
+ counts,
63
+ unread: countUnreadContactMessages(dataDir),
64
+ tabs: MESSAGE_TABS.map((tab) => ({
65
+ ...tab,
66
+ url: listUrl(tab.status),
67
+ count: counts[tab.status],
68
+ current: tab.status === status,
69
+ })),
70
+ rows,
71
+ fields: MESSAGE_FIELDS,
72
+ readUrl: MESSAGES_READ_PATH,
73
+ deleteUrl: MESSAGES_DELETE_PATH,
74
+ page,
75
+ pages,
76
+ previousUrl: page > 1 ? listUrl(status, page - 1) : undefined,
77
+ nextUrl: page < pages ? listUrl(status, page + 1) : undefined,
78
+ });
79
+ });
80
+ app.post(MESSAGES_READ_PATH, async (c) => {
81
+ const body = await c.req.parseBody();
82
+ const back = listUrl(statusOf(text(body[MESSAGE_FIELDS.status])));
83
+ // A checkbox submits nothing at all when it is clear, which is what the
84
+ // empty string here means: this button marks the message unread again.
85
+ const read = text(body[MESSAGE_FIELDS.read]) !== '';
86
+ const updated = await setContactMessageRead(c.var.config.dataDir, text(body[MESSAGE_FIELDS.id]), read);
87
+ if (updated === undefined) {
88
+ flash(c, 'error', 'That message is not here any more.');
89
+ return c.redirect(back, 303);
90
+ }
91
+ flash(c, 'notice', read
92
+ ? `Marked ${updated.from.name}’s message read.`
93
+ : `Marked ${updated.from.name}’s message unread.`);
94
+ return c.redirect(back, 303);
95
+ });
96
+ app.post(MESSAGES_DELETE_PATH, async (c) => {
97
+ const body = await c.req.parseBody();
98
+ const back = listUrl(statusOf(text(body[MESSAGE_FIELDS.status])));
99
+ const id = text(body[MESSAGE_FIELDS.id]);
100
+ // Read before it goes, so the flash can say whose message it was. An id
101
+ // that is not one never reaches the filesystem: the records module refuses
102
+ // anything but the shape it mints.
103
+ const held = listContactMessages(c.var.config.dataDir).find((message) => message.id === id)
104
+ ?.from.name;
105
+ if (!(await deleteContactMessage(c.var.config.dataDir, id))) {
106
+ flash(c, 'error', 'That message is not here any more.');
107
+ return c.redirect(back, 303);
108
+ }
109
+ flash(c, 'notice', `Deleted ${held ?? 'the'} message.`);
110
+ return c.redirect(back, 303);
111
+ });
112
+ }
113
+ /** How many messages are waiting to be read, which is the number on the dashboard. */
114
+ export function unreadMessages(dataDir) {
115
+ return countUnreadContactMessages(dataDir);
116
+ }
117
+ /** Where one list of the inbox lives. */
118
+ export function listUrl(status, page = 1) {
119
+ const query = page > 1 ? `&page=${String(page)}` : '';
120
+ return `${MESSAGES_PATH}?status=${status}${query}`;
121
+ }
122
+ /** A requested status, or the inbox for anything else. */
123
+ function statusOf(value) {
124
+ return value === 'spam' ? 'spam' : 'received';
125
+ }
126
+ /** One stored message as the screen shows it. */
127
+ function messageRow(message, timezone) {
128
+ const subject = message.subject === '' ? `A message from ${message.from.name}` : message.subject;
129
+ return {
130
+ id: message.id,
131
+ status: message.status,
132
+ read: message.read,
133
+ author: message.from.name,
134
+ email: message.from.email,
135
+ subject: message.subject,
136
+ message: message.message,
137
+ received: formatInTimezone(message.received, timezone),
138
+ // The first eight characters are enough to see that two messages came from
139
+ // one place, which is the only question the hash is there to answer.
140
+ address: message.addressHash === null ? null : message.addressHash.slice(0, 8),
141
+ page: message.page.title === '' ? message.page.slug : message.page.title,
142
+ pageUrl: message.page.permalink === '' ? null : message.page.permalink,
143
+ // Answering from here rather than from a mail client is one click; the
144
+ // subject comes back with `Re:` on it the way a reply would.
145
+ replyUrl: `mailto:${encodeURIComponent(message.from.email)}?subject=${encodeURIComponent(`Re: ${subject}`)}`,
146
+ };
147
+ }
148
+ /** A form field as a string. A file upload, or a missing field, is the empty one. */
149
+ function text(value) {
150
+ return typeof value === 'string' ? value : '';
151
+ }
152
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/admin/messages.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/admin/preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,MAAM,CAAC;AAM1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAM5C,8DAA8D;AAC9D,eAAO,MAAM,YAAY,mBAA4B,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAYxD"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/admin/preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,MAAM,CAAC;AAO1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAS5C,8DAA8D;AAC9D,eAAO,MAAM,YAAY,mBAA4B,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAqBxD"}
@@ -1,9 +1,13 @@
1
1
  import { renderMarkdown } from '../content/markdown.js';
2
2
  import { defaultPermalink, slugify } from '../content/slug.js';
3
+ import { calendarDayIn, toUtcInstant } from '../content/time.js';
3
4
  import { normalizeBody } from '../content/writer.js';
5
+ import { authorContext } from '../web/authors.js';
4
6
  import { documentContext } from '../web/context.js';
7
+ import { findUserById, listUsers } from './accounts.js';
5
8
  import { TEMPLATES } from '../web/render.js';
6
9
  import { splitTags } from './documents.js';
10
+ import { readSiteSettings } from './settings.js';
7
11
  import { ADMIN_PREFIX } from './session.js';
8
12
  /** Where the editor posts a body it wants to see rendered. */
9
13
  export const PREVIEW_PATH = `${ADMIN_PREFIX}/preview`;
@@ -23,14 +27,24 @@ export function mountPreview(app) {
23
27
  app.post(PREVIEW_PATH, async (c) => {
24
28
  const body = await c.req.parseBody();
25
29
  const type = text(body['type']) === 'page' ? 'page' : 'post';
26
- return c.html(c.var.renderer.render(type === 'post' ? TEMPLATES.post : TEMPLATES.page, documentContext(previewDocument(c, { type, body }))));
30
+ const document = previewDocument(c, { type, body });
31
+ return c.html(c.var.renderer.render(type === 'post' ? TEMPLATES.post : TEMPLATES.page,
32
+ // With the site's image config, so a preview shows the `<picture>` the
33
+ // published page would show rather than the plain image behind it, and
34
+ // with the author resolved, so the byline reads the way the published
35
+ // page's would (TASK-67).
36
+ documentContext(document, c.var.config, authorContext(listUsers(c.var.config.dataDir), document.author))));
27
37
  });
28
38
  }
29
39
  /** What a submitted editor form looks like as a document nobody has saved. */
30
40
  function previewDocument(c, input) {
31
41
  const { type, body } = input;
42
+ const timezone = readSiteSettings(c.var.config.contentDir).timezone;
32
43
  const title = text(body['title']).trim();
33
- const date = type === 'post' ? orNow(text(body['date']).trim()) : undefined;
44
+ // The editor's field is wall-clock time in the site's zone (decision-11), so
45
+ // the preview reads it the way the save will and shows the date the saved
46
+ // post would show rather than one the server's own zone invented.
47
+ const date = type === 'post' ? orNow(text(body['date']).trim(), timezone, c.var.store.now()) : undefined;
34
48
  const slug = slugify(text(body['slug']).trim()) || slugify(title) || 'preview';
35
49
  const markdown = normalizeBody(text(body['body']));
36
50
  return {
@@ -39,10 +53,16 @@ function previewDocument(c, input) {
39
53
  // prints them; neither is ever written anywhere.
40
54
  path: `${type === 'post' ? 'posts' : 'pages'}/${slug}.md`,
41
55
  slug,
42
- permalink: permalinkFor({ type, slug, date, submitted: text(body['permalink']).trim() }),
56
+ permalink: permalinkFor({
57
+ type,
58
+ slug,
59
+ date: date === undefined ? undefined : (calendarDayIn(date, timezone) ?? date),
60
+ submitted: text(body['permalink']).trim(),
61
+ }),
43
62
  title: title === '' ? 'Untitled' : title,
44
63
  ...(date === undefined ? {} : { date }),
45
64
  tags: splitTags(text(body['tags'])),
65
+ categories: splitTags(text(body['categories'])),
46
66
  draft: body['draft'] !== undefined,
47
67
  ...(text(body['description']) === '' ? {} : { description: text(body['description']).trim() }),
48
68
  ...(currentUsername(c) === undefined ? {} : { author: currentUsername(c) }),
@@ -65,16 +85,20 @@ function permalinkFor(input) {
65
85
  return '/';
66
86
  }
67
87
  }
68
- /** A date field that is empty or unusable becomes now, so the layout has one. */
69
- function orNow(value) {
70
- return /^\d{4}-\d{2}-\d{2}/.test(value) ? value : new Date().toISOString();
88
+ /**
89
+ * The date field as a UTC instant. One that is empty or unusable becomes now,
90
+ * so the layout always has one to print.
91
+ */
92
+ function orNow(value, timezone, now) {
93
+ const instant = /^\d{4}-\d{2}-\d{2}/.test(value) ? toUtcInstant(value, timezone) : undefined;
94
+ return instant ?? now.toISOString();
71
95
  }
72
96
  /** The signed-in user's login, so a byline in the theme has something to print. */
73
97
  function currentUsername(c) {
74
98
  const userId = c.var.session?.userId;
75
99
  if (userId == null)
76
100
  return undefined;
77
- return c.var.admin.getUserById(userId)?.username;
101
+ return findUserById(c.var.config.dataDir, userId)?.username;
78
102
  }
79
103
  /** A form field as a string. A file upload, or a missing field, is the empty one. */
80
104
  function text(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"preview.js","sourceRoot":"","sources":["../../src/admin/preview.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"preview.js","sourceRoot":"","sources":["../../src/admin/preview.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ import type { Context, Hono } from 'hono';
2
+ import type { ResolvedConfig } from '../config.ts';
3
+ import type { GeekityEnv } from '../env.ts';
4
+ import type { AdminRender } from './documents.ts';
5
+ import type { Session } from './store.ts';
6
+ import type { LoginThrottle } from './throttle.ts';
7
+ /**
8
+ * Forgetting a password, and getting back in without a shell.
9
+ *
10
+ * Two unauthenticated screens, built to the OWASP Forgot Password guidance,
11
+ * and four rules that between them are the whole design:
12
+ *
13
+ * - **One answer.** The request form says exactly the same thing to a name
14
+ * that exists, a name that does not, and a name whose user has no email
15
+ * address. A recovery form that answered differently would be a list of
16
+ * which accounts a site has, handed to anybody who asked.
17
+ * - **The link is the secret.** 256 random bits, in the URL, sent to an
18
+ * address somebody put on their own account. The database keeps only its
19
+ * SHA-256 (see the `password_resets` migration), so a copy of the cache is
20
+ * not a stack of working links, and the token is never rendered into the
21
+ * page of the browser that asked for it — only into the message.
22
+ * - **It is spent when it is used.** Setting a password deletes every reset
23
+ * that user had outstanding and ends every session they had, because the
24
+ * ordinary reason to reset a password is that somebody else may have had it.
25
+ * - **It cannot be a lever.** Requests are rate limited exactly as sign-ins
26
+ * are (TASK-47), on a throttle of their own, so a flood of recovery requests
27
+ * for one username cannot lock that person out of logging in.
28
+ */
29
+ /** Where the request-a-link form lives. */
30
+ export declare const FORGOT_PATH = "/admin/forgot";
31
+ /** Where the set-a-new-password form lives. The link points here. */
32
+ export declare const RESET_PATH = "/admin/reset";
33
+ /** The fields the two forms submit. */
34
+ export declare const RECOVERY_FIELDS: {
35
+ /** A username or an email address; the form does not ask which. */
36
+ readonly identifier: 'identifier';
37
+ /** The token, carried from the link's query string into the form. */
38
+ readonly token: 'token';
39
+ readonly password: 'password';
40
+ readonly passwordConfirmation: 'password_confirmation';
41
+ };
42
+ /**
43
+ * How long a link lasts, in seconds.
44
+ *
45
+ * An hour, which is what the task asks for and what OWASP calls a reasonable
46
+ * upper bound: long enough to walk to another machine and read the mail there,
47
+ * short enough that a link left in an inbox is not a spare key.
48
+ */
49
+ export declare const RESET_TOKEN_LIFETIME_SECONDS = 3600;
50
+ /** The message asking somebody whether they meant to do this. */
51
+ export declare const RESET_TEMPLATE = "password-reset";
52
+ /** The message telling them it happened. */
53
+ export declare const PASSWORD_CHANGED_TEMPLATE = "password-changed";
54
+ /**
55
+ * The one thing the request form ever says.
56
+ *
57
+ * Spelled once, and rendered whatever happened, so the two paths through the
58
+ * handler cannot drift into two different answers.
59
+ */
60
+ export declare const RECOVERY_ANSWER: string;
61
+ /** What {@link mountRecovery} needs from the admin around it. */
62
+ export interface MountRecoveryOptions {
63
+ /** The admin's renderer, which injects the chrome, the CSRF token and the flash. */
64
+ render: AdminRender;
65
+ /**
66
+ * A session to hang a CSRF token on, made on the spot if the visitor has
67
+ * none. The same one the login and setup forms use: these pages are reached
68
+ * by somebody with no login, and a form with no session has no token.
69
+ */
70
+ anonymousSession: (c: Context<GeekityEnv>) => Session;
71
+ /**
72
+ * The throttle recovery requests are counted against.
73
+ *
74
+ * Its own, not the login one. Sharing would mean a stranger could lock
75
+ * somebody out of signing in by asking for their password to be reset over
76
+ * and over — turning a recovery feature into a denial of service against the
77
+ * person it exists to help.
78
+ */
79
+ throttle: (config: ResolvedConfig) => LoginThrottle;
80
+ }
81
+ /** Register the forgot-password and reset screens. */
82
+ export declare function mountRecovery(app: Hono<GeekityEnv>, options: MountRecoveryOptions): void;
83
+ /**
84
+ * What is wrong with the password somebody typed into the reset form.
85
+ *
86
+ * The same rules the change-password form and `geekity user add` enforce, so a
87
+ * password set through a reset link is one every other door would have taken.
88
+ */
89
+ export declare function newPasswordProblem(password: string, confirmation: string): string | undefined;
90
+ /** The link that goes in the message, absolute because it is read elsewhere. */
91
+ export declare function resetLink(baseUrl: string, token: string): string;
92
+ //# sourceMappingURL=recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../src/admin/recovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAG5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,2CAA2C;AAC3C,eAAO,MAAM,WAAW,kBAA2B,CAAC;AAEpD,qEAAqE;AACrE,eAAO,MAAM,UAAU,iBAA0B,CAAC;AAElD,uCAAuC;AACvC,eAAO,MAAM,eAAe;IAC1B,mEAAmE;aACnE,UAAU,EAAE,YAAY;IACxB,qEAAqE;aACrE,KAAK,EAAE,OAAO;aACd,QAAQ,EAAE,UAAU;aACpB,oBAAoB,EAAE,uBAAuB;CACrC,CAAC;AAEX;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,iEAAiE;AACjE,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C,4CAA4C;AAC5C,eAAO,MAAM,yBAAyB,qBAAqB,CAAC;AAE5D;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAEoE,CAAC;AAEjG,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC,oFAAoF;IACpF,MAAM,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,KAAK,OAAO,CAAC;IACtD;;;;;;;OAOG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,aAAa,CAAC;CACrD;AAED,sDAAsD;AACtD,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,oBAAoB,GAAG,IAAI,CA6JxF;AAiBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK7F;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhE"}
@@ -0,0 +1,229 @@
1
+ import { findUserById, findUserByIdentifier, setUserPassword } from './accounts.js';
2
+ import { passwordProblem } from './credentials.js';
3
+ import { flash } from './flash.js';
4
+ import { ADMIN_PREFIX } from './session.js';
5
+ import { ADMIN_TEMPLATES } from './templates.js';
6
+ import { clientAddress, describeWait, loginKeys } from './throttle.js';
7
+ /**
8
+ * Forgetting a password, and getting back in without a shell.
9
+ *
10
+ * Two unauthenticated screens, built to the OWASP Forgot Password guidance,
11
+ * and four rules that between them are the whole design:
12
+ *
13
+ * - **One answer.** The request form says exactly the same thing to a name
14
+ * that exists, a name that does not, and a name whose user has no email
15
+ * address. A recovery form that answered differently would be a list of
16
+ * which accounts a site has, handed to anybody who asked.
17
+ * - **The link is the secret.** 256 random bits, in the URL, sent to an
18
+ * address somebody put on their own account. The database keeps only its
19
+ * SHA-256 (see the `password_resets` migration), so a copy of the cache is
20
+ * not a stack of working links, and the token is never rendered into the
21
+ * page of the browser that asked for it — only into the message.
22
+ * - **It is spent when it is used.** Setting a password deletes every reset
23
+ * that user had outstanding and ends every session they had, because the
24
+ * ordinary reason to reset a password is that somebody else may have had it.
25
+ * - **It cannot be a lever.** Requests are rate limited exactly as sign-ins
26
+ * are (TASK-47), on a throttle of their own, so a flood of recovery requests
27
+ * for one username cannot lock that person out of logging in.
28
+ */
29
+ /** Where the request-a-link form lives. */
30
+ export const FORGOT_PATH = `${ADMIN_PREFIX}/forgot`;
31
+ /** Where the set-a-new-password form lives. The link points here. */
32
+ export const RESET_PATH = `${ADMIN_PREFIX}/reset`;
33
+ /** The fields the two forms submit. */
34
+ export const RECOVERY_FIELDS = {
35
+ /** A username or an email address; the form does not ask which. */
36
+ identifier: 'identifier',
37
+ /** The token, carried from the link's query string into the form. */
38
+ token: 'token',
39
+ password: 'password',
40
+ passwordConfirmation: 'password_confirmation',
41
+ };
42
+ /**
43
+ * How long a link lasts, in seconds.
44
+ *
45
+ * An hour, which is what the task asks for and what OWASP calls a reasonable
46
+ * upper bound: long enough to walk to another machine and read the mail there,
47
+ * short enough that a link left in an inbox is not a spare key.
48
+ */
49
+ export const RESET_TOKEN_LIFETIME_SECONDS = 3600;
50
+ /** The message asking somebody whether they meant to do this. */
51
+ export const RESET_TEMPLATE = 'password-reset';
52
+ /** The message telling them it happened. */
53
+ export const PASSWORD_CHANGED_TEMPLATE = 'password-changed';
54
+ /**
55
+ * The one thing the request form ever says.
56
+ *
57
+ * Spelled once, and rendered whatever happened, so the two paths through the
58
+ * handler cannot drift into two different answers.
59
+ */
60
+ export const RECOVERY_ANSWER = 'If that matches an account with an email address on it, a link to set a new password is on ' +
61
+ 'its way. The link lasts an hour and works once. Check the spam folder if it does not arrive.';
62
+ /** Register the forgot-password and reset screens. */
63
+ export function mountRecovery(app, options) {
64
+ const { render, anonymousSession, throttle } = options;
65
+ app.get(FORGOT_PATH, (c) => {
66
+ anonymousSession(c);
67
+ return render(c, ADMIN_TEMPLATES.forgot, forgotScreen(c));
68
+ });
69
+ app.post(FORGOT_PATH, async (c) => {
70
+ const config = c.var.config;
71
+ const body = await c.req.parseBody();
72
+ const identifier = field(body[RECOVERY_FIELDS.identifier]).trim();
73
+ const address = clientAddress(c, config);
74
+ const keys = loginKeys(identifier, address);
75
+ const throttled = throttle(config);
76
+ const wait = throttled.retryAfter(keys);
77
+ if (wait !== undefined) {
78
+ anonymousSession(c);
79
+ c.status(429);
80
+ c.header('Retry-After', String(wait));
81
+ return render(c, ADMIN_TEMPLATES.forgot,
82
+ // Says nothing about whether that account exists: an unknown name is
83
+ // counted and locked out exactly as a real one is.
84
+ forgotScreen(c, { error: `Too many requests. Try again in ${describeWait(wait)}.` }));
85
+ }
86
+ // Every request is counted, not only the ones that matched: counting a
87
+ // match differently would make the lockout itself the answer the rest of
88
+ // this handler refuses to give.
89
+ throttled.fail(keys);
90
+ const user = identifier === '' ? undefined : findUserByIdentifier(config.dataDir, identifier);
91
+ if (user?.email !== undefined && c.var.mail.configured()) {
92
+ // A good moment to sweep: resets are rare, and this is the one handler
93
+ // that writes a row to the table.
94
+ c.var.admin.prunePasswordResets();
95
+ const reset = c.var.admin.createPasswordReset({
96
+ userId: user.id,
97
+ lifetimeSeconds: RESET_TOKEN_LIFETIME_SECONDS,
98
+ });
99
+ // Dropped rather than awaited, for two reasons: the answer is the same
100
+ // whatever the provider says, and waiting for a provider on one path and
101
+ // not the other is how the page's timing would start telling people
102
+ // which usernames exist.
103
+ void c.var.mail.send({
104
+ to: user.email,
105
+ template: RESET_TEMPLATE,
106
+ data: {
107
+ username: user.username,
108
+ resetUrl: resetLink(config.baseUrl, reset.token),
109
+ expiresAt: reset.expiresAt,
110
+ expiresInHours: RESET_TOKEN_LIFETIME_SECONDS / 3600,
111
+ },
112
+ });
113
+ }
114
+ anonymousSession(c);
115
+ return render(c, ADMIN_TEMPLATES.forgot, forgotScreen(c, { sent: true }));
116
+ });
117
+ app.get(RESET_PATH, (c) => {
118
+ anonymousSession(c);
119
+ const token = c.req.query(RECOVERY_FIELDS.token) ?? '';
120
+ const reset = token === '' ? undefined : c.var.admin.getPasswordReset(token);
121
+ if (reset === undefined)
122
+ return refuseToken(c);
123
+ return render(c, ADMIN_TEMPLATES.reset, resetScreen(token));
124
+ });
125
+ app.post(RESET_PATH, async (c) => {
126
+ const dataDir = c.var.config.dataDir;
127
+ const body = await c.req.parseBody();
128
+ const token = field(body[RECOVERY_FIELDS.token]);
129
+ const password = field(body[RECOVERY_FIELDS.password]);
130
+ const confirmation = field(body[RECOVERY_FIELDS.passwordConfirmation]);
131
+ // Checked before the password rules, so a stale link is a stale link
132
+ // whatever was typed into it — and so a refused form never spends one.
133
+ const reset = token === '' ? undefined : c.var.admin.getPasswordReset(token);
134
+ if (reset === undefined)
135
+ return refuseToken(c);
136
+ const problem = newPasswordProblem(password, confirmation);
137
+ if (problem !== undefined) {
138
+ c.status(400);
139
+ // Nothing typed is echoed back: both fields are passwords.
140
+ return render(c, ADMIN_TEMPLATES.reset, resetScreen(token, { problem }));
141
+ }
142
+ const user = findUserById(dataDir, reset.userId);
143
+ if (user === undefined) {
144
+ // The account went while the link was in flight. The token is worthless
145
+ // now, and saying so beats a 500 from a write against nobody.
146
+ c.var.admin.deletePasswordResetsForUser(reset.userId);
147
+ return refuseToken(c);
148
+ }
149
+ await setUserPassword({ dataDir, userId: user.id, password });
150
+ // Everything that was a way into this account is now closed: the link that
151
+ // was just used, every other link asked for while it was in flight, and
152
+ // every browser that was signed in. Nobody is spared, because the person
153
+ // doing this is not signed in — they are about to be.
154
+ c.var.admin.deletePasswordResetsForUser(user.id);
155
+ const ended = c.var.admin.deleteSessionsForUser(user.id);
156
+ if (user.email !== undefined) {
157
+ // The other half of the OWASP guidance: whoever owns the address hears
158
+ // that the password changed, so a reset they did not ask for is
159
+ // something they find out about rather than something they discover.
160
+ void c.var.mail.send({
161
+ to: user.email,
162
+ template: PASSWORD_CHANGED_TEMPLATE,
163
+ data: { username: user.username, signedOut: ended },
164
+ });
165
+ }
166
+ flash(c, 'notice', 'Your password was changed. Sign in with the new one.');
167
+ return c.redirect(LOGIN_REDIRECT, 303);
168
+ });
169
+ /** What both handlers say to a token that buys nothing. */
170
+ function refuseToken(c) {
171
+ anonymousSession(c);
172
+ c.status(400);
173
+ return render(c, ADMIN_TEMPLATES.reset, {
174
+ forgotUrl: FORGOT_PATH,
175
+ resetUrl: RESET_PATH,
176
+ fields: RECOVERY_FIELDS,
177
+ invalid: true,
178
+ });
179
+ }
180
+ /** Everything the forgot-password template renders. */
181
+ function forgotScreen(c, extra = {}) {
182
+ return {
183
+ forgotUrl: FORGOT_PATH,
184
+ loginUrl: LOGIN_REDIRECT,
185
+ fields: RECOVERY_FIELDS,
186
+ // Read per request, exactly as the mail service reads it: a credential
187
+ // pasted into the settings screen turns recovery on for the next visitor
188
+ // without a restart.
189
+ configured: c.var.mail.configured(),
190
+ answer: RECOVERY_ANSWER,
191
+ sent: false,
192
+ ...extra,
193
+ };
194
+ }
195
+ }
196
+ /** Where a finished reset sends the browser. Spelled here to avoid a cycle. */
197
+ const LOGIN_REDIRECT = `${ADMIN_PREFIX}/login`;
198
+ /** Everything the reset template renders for a link that is still good. */
199
+ function resetScreen(token, extra = {}) {
200
+ return {
201
+ forgotUrl: FORGOT_PATH,
202
+ resetUrl: RESET_PATH,
203
+ fields: RECOVERY_FIELDS,
204
+ token,
205
+ invalid: false,
206
+ ...extra,
207
+ };
208
+ }
209
+ /**
210
+ * What is wrong with the password somebody typed into the reset form.
211
+ *
212
+ * The same rules the change-password form and `geekity user add` enforce, so a
213
+ * password set through a reset link is one every other door would have taken.
214
+ */
215
+ export function newPasswordProblem(password, confirmation) {
216
+ return (passwordProblem(password) ??
217
+ (password === confirmation ? undefined : 'The two passwords do not match.'));
218
+ }
219
+ /** The link that goes in the message, absolute because it is read elsewhere. */
220
+ export function resetLink(baseUrl, token) {
221
+ const url = new URL(RESET_PATH, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
222
+ url.searchParams.set(RECOVERY_FIELDS.token, token);
223
+ return url.href;
224
+ }
225
+ /** A form field as a string. A file upload, or a missing field, is the empty one. */
226
+ function field(value) {
227
+ return typeof value === 'string' ? value : '';
228
+ }
229
+ //# sourceMappingURL=recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.js","sourceRoot":"","sources":["../../src/admin/recovery.ts"],"names":[],"mappings":"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"}
@@ -6,26 +6,6 @@ export declare const LOGIN_PATH = "/admin/login";
6
6
  export declare const SETUP_PATH = "/admin/setup";
7
7
  /** Where the logout form posts. */
8
8
  export declare const LOGOUT_PATH = "/admin/logout";
9
- /** Where the dashboard's quick draft form posts. */
10
- export declare const QUICK_DRAFT_PATH = "/admin/quick-draft";
11
- /** One entry in the admin's left-hand navigation. */
12
- export interface AdminSection {
13
- /** The name a screen passes as `section` to mark itself current. */
14
- section: string;
15
- /** What the link says. */
16
- label: string;
17
- /** Where it goes. */
18
- url: string;
19
- }
20
- /**
21
- * The sections of the admin, in the order doc-5 lists them.
22
- *
23
- * Every screen renders the same list and marks one of them, so a screen added
24
- * later only has to name its section. The screens behind most of these links
25
- * are still placeholders; the navigation is the shape of the whole admin, not
26
- * of the part that is built.
27
- */
28
- export declare const ADMIN_SECTIONS: readonly AdminSection[];
29
9
  /** How many recent posts the dashboard lists. */
30
10
  export declare const DASHBOARD_RECENT_POSTS = 5;
31
11
  /** Where the editor for a post lives. */