@geekity/cms 0.3.0 → 0.5.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 (660) hide show
  1. package/README.md +2217 -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/{layouts/document-editor.njk → pages/documents/editor.njk} +75 -59
  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/federation/settings.njk +77 -0
  19. package/admin/pages/media/library.njk +160 -0
  20. package/admin/pages/messages/all.njk +110 -0
  21. package/admin/pages/navigation/menus.njk +129 -0
  22. package/admin/{layouts → pages}/placeholder.njk +5 -1
  23. package/admin/pages/settings/discussion.njk +99 -0
  24. package/admin/pages/settings/email.njk +203 -0
  25. package/admin/pages/settings/general.njk +29 -0
  26. package/admin/pages/settings/permalinks.njk +50 -0
  27. package/admin/pages/settings/reading.njk +64 -0
  28. package/admin/pages/tools/content-index.njk +89 -0
  29. package/admin/pages/users/edit.njk +239 -0
  30. package/admin/pages/users/list.njk +51 -0
  31. package/admin/pages/users/new.njk +41 -0
  32. package/admin/static/admin.css +558 -3
  33. package/admin/static/copy.js +52 -0
  34. package/admin/static/editor.js +16 -16
  35. package/admin/static/slug.js +61 -0
  36. package/dist/access-log.d.ts +44 -0
  37. package/dist/access-log.d.ts.map +1 -0
  38. package/dist/access-log.js +71 -0
  39. package/dist/access-log.js.map +1 -0
  40. package/dist/admin/accounts.d.ts +413 -0
  41. package/dist/admin/accounts.d.ts.map +1 -0
  42. package/dist/admin/accounts.js +680 -0
  43. package/dist/admin/accounts.js.map +1 -0
  44. package/dist/admin/appearance.d.ts +80 -0
  45. package/dist/admin/appearance.d.ts.map +1 -0
  46. package/dist/admin/appearance.js +136 -0
  47. package/dist/admin/appearance.js.map +1 -0
  48. package/dist/admin/comments.d.ts +90 -0
  49. package/dist/admin/comments.d.ts.map +1 -0
  50. package/dist/admin/comments.js +244 -0
  51. package/dist/admin/comments.js.map +1 -0
  52. package/dist/admin/credentials.d.ts +26 -2
  53. package/dist/admin/credentials.d.ts.map +1 -1
  54. package/dist/admin/credentials.js +35 -2
  55. package/dist/admin/credentials.js.map +1 -1
  56. package/dist/admin/documents.d.ts +117 -10
  57. package/dist/admin/documents.d.ts.map +1 -1
  58. package/dist/admin/documents.js +328 -41
  59. package/dist/admin/documents.js.map +1 -1
  60. package/dist/admin/federation.d.ts +161 -50
  61. package/dist/admin/federation.d.ts.map +1 -1
  62. package/dist/admin/federation.js +235 -93
  63. package/dist/admin/federation.js.map +1 -1
  64. package/dist/admin/formatting.d.ts +20 -0
  65. package/dist/admin/formatting.d.ts.map +1 -0
  66. package/dist/admin/formatting.js +72 -0
  67. package/dist/admin/formatting.js.map +1 -0
  68. package/dist/admin/headers.d.ts +65 -0
  69. package/dist/admin/headers.d.ts.map +1 -0
  70. package/dist/admin/headers.js +104 -0
  71. package/dist/admin/headers.js.map +1 -0
  72. package/dist/admin/index.d.ts +38 -12
  73. package/dist/admin/index.d.ts.map +1 -1
  74. package/dist/admin/index.js +25 -7
  75. package/dist/admin/index.js.map +1 -1
  76. package/dist/admin/media.d.ts +168 -0
  77. package/dist/admin/media.d.ts.map +1 -0
  78. package/dist/admin/media.js +295 -0
  79. package/dist/admin/media.js.map +1 -0
  80. package/dist/admin/menu.d.ts +79 -0
  81. package/dist/admin/menu.d.ts.map +1 -0
  82. package/dist/admin/menu.js +143 -0
  83. package/dist/admin/menu.js.map +1 -0
  84. package/dist/admin/messages.d.ts +80 -0
  85. package/dist/admin/messages.d.ts.map +1 -0
  86. package/dist/admin/messages.js +152 -0
  87. package/dist/admin/messages.js.map +1 -0
  88. package/dist/admin/navigation.d.ts +155 -0
  89. package/dist/admin/navigation.d.ts.map +1 -0
  90. package/dist/admin/navigation.js +292 -0
  91. package/dist/admin/navigation.js.map +1 -0
  92. package/dist/admin/preview.d.ts.map +1 -1
  93. package/dist/admin/preview.js +31 -7
  94. package/dist/admin/preview.js.map +1 -1
  95. package/dist/admin/recovery.d.ts +92 -0
  96. package/dist/admin/recovery.d.ts.map +1 -0
  97. package/dist/admin/recovery.js +229 -0
  98. package/dist/admin/recovery.js.map +1 -0
  99. package/dist/admin/routes.d.ts +0 -20
  100. package/dist/admin/routes.d.ts.map +1 -1
  101. package/dist/admin/routes.js +164 -105
  102. package/dist/admin/routes.js.map +1 -1
  103. package/dist/admin/session.d.ts +17 -3
  104. package/dist/admin/session.d.ts.map +1 -1
  105. package/dist/admin/session.js +19 -5
  106. package/dist/admin/session.js.map +1 -1
  107. package/dist/admin/settings-discussion.d.ts +37 -0
  108. package/dist/admin/settings-discussion.d.ts.map +1 -0
  109. package/dist/admin/settings-discussion.js +117 -0
  110. package/dist/admin/settings-discussion.js.map +1 -0
  111. package/dist/admin/settings-email.d.ts +54 -0
  112. package/dist/admin/settings-email.d.ts.map +1 -0
  113. package/dist/admin/settings-email.js +189 -0
  114. package/dist/admin/settings-email.js.map +1 -0
  115. package/dist/admin/settings-federation.d.ts +48 -0
  116. package/dist/admin/settings-federation.d.ts.map +1 -0
  117. package/dist/admin/settings-federation.js +123 -0
  118. package/dist/admin/settings-federation.js.map +1 -0
  119. package/dist/admin/settings-general.d.ts +12 -0
  120. package/dist/admin/settings-general.d.ts.map +1 -0
  121. package/dist/admin/settings-general.js +48 -0
  122. package/dist/admin/settings-general.js.map +1 -0
  123. package/dist/admin/settings-page.d.ts +119 -0
  124. package/dist/admin/settings-page.d.ts.map +1 -0
  125. package/dist/admin/settings-page.js +132 -0
  126. package/dist/admin/settings-page.js.map +1 -0
  127. package/dist/admin/settings-pages.d.ts +30 -0
  128. package/dist/admin/settings-pages.d.ts.map +1 -0
  129. package/dist/admin/settings-pages.js +43 -0
  130. package/dist/admin/settings-pages.js.map +1 -0
  131. package/dist/admin/settings-permalinks.d.ts +16 -0
  132. package/dist/admin/settings-permalinks.d.ts.map +1 -0
  133. package/dist/admin/settings-permalinks.js +32 -0
  134. package/dist/admin/settings-permalinks.js.map +1 -0
  135. package/dist/admin/settings-reading.d.ts +34 -0
  136. package/dist/admin/settings-reading.d.ts.map +1 -0
  137. package/dist/admin/settings-reading.js +49 -0
  138. package/dist/admin/settings-reading.js.map +1 -0
  139. package/dist/admin/settings.d.ts +339 -96
  140. package/dist/admin/settings.d.ts.map +1 -1
  141. package/dist/admin/settings.js +600 -308
  142. package/dist/admin/settings.js.map +1 -1
  143. package/dist/admin/store.d.ts +606 -152
  144. package/dist/admin/store.d.ts.map +1 -1
  145. package/dist/admin/store.js +863 -268
  146. package/dist/admin/store.js.map +1 -1
  147. package/dist/admin/taxonomy.d.ts +114 -0
  148. package/dist/admin/taxonomy.d.ts.map +1 -0
  149. package/dist/admin/taxonomy.js +289 -0
  150. package/dist/admin/taxonomy.js.map +1 -0
  151. package/dist/admin/templates.d.ts +46 -14
  152. package/dist/admin/templates.d.ts.map +1 -1
  153. package/dist/admin/templates.js +46 -14
  154. package/dist/admin/templates.js.map +1 -1
  155. package/dist/admin/throttle.d.ts +84 -0
  156. package/dist/admin/throttle.d.ts.map +1 -0
  157. package/dist/admin/throttle.js +143 -0
  158. package/dist/admin/throttle.js.map +1 -0
  159. package/dist/admin/tools.d.ts +111 -0
  160. package/dist/admin/tools.d.ts.map +1 -0
  161. package/dist/admin/tools.js +182 -0
  162. package/dist/admin/tools.js.map +1 -0
  163. package/dist/admin/uploads.d.ts +28 -2
  164. package/dist/admin/uploads.d.ts.map +1 -1
  165. package/dist/admin/uploads.js +43 -4
  166. package/dist/admin/uploads.js.map +1 -1
  167. package/dist/admin/users.d.ts +100 -13
  168. package/dist/admin/users.d.ts.map +1 -1
  169. package/dist/admin/users.js +412 -39
  170. package/dist/admin/users.js.map +1 -1
  171. package/dist/cache.d.ts +141 -0
  172. package/dist/cache.d.ts.map +1 -0
  173. package/dist/cache.js +207 -0
  174. package/dist/cache.js.map +1 -0
  175. package/dist/cli.d.ts +14 -2
  176. package/dist/cli.d.ts.map +1 -1
  177. package/dist/cli.js +429 -21
  178. package/dist/cli.js.map +1 -1
  179. package/dist/comments/akismet.d.ts +130 -0
  180. package/dist/comments/akismet.d.ts.map +1 -0
  181. package/dist/comments/akismet.js +274 -0
  182. package/dist/comments/akismet.js.map +1 -0
  183. package/dist/comments/conversation.d.ts +29 -0
  184. package/dist/comments/conversation.d.ts.map +1 -0
  185. package/dist/comments/conversation.js +60 -0
  186. package/dist/comments/conversation.js.map +1 -0
  187. package/dist/comments/form.d.ts +123 -0
  188. package/dist/comments/form.d.ts.map +1 -0
  189. package/dist/comments/form.js +79 -0
  190. package/dist/comments/form.js.map +1 -0
  191. package/dist/comments/index.d.ts +16 -0
  192. package/dist/comments/index.d.ts.map +1 -0
  193. package/dist/comments/index.js +10 -0
  194. package/dist/comments/index.js.map +1 -0
  195. package/dist/comments/markdown.d.ts +8 -0
  196. package/dist/comments/markdown.d.ts.map +1 -0
  197. package/dist/comments/markdown.js +85 -0
  198. package/dist/comments/markdown.js.map +1 -0
  199. package/dist/comments/moderate.d.ts +71 -0
  200. package/dist/comments/moderate.d.ts.map +1 -0
  201. package/dist/comments/moderate.js +88 -0
  202. package/dist/comments/moderate.js.map +1 -0
  203. package/dist/comments/policy.d.ts +57 -0
  204. package/dist/comments/policy.d.ts.map +1 -0
  205. package/dist/comments/policy.js +80 -0
  206. package/dist/comments/policy.js.map +1 -0
  207. package/dist/comments/records.d.ts +266 -0
  208. package/dist/comments/records.d.ts.map +1 -0
  209. package/dist/comments/records.js +487 -0
  210. package/dist/comments/records.js.map +1 -0
  211. package/dist/comments/routes.d.ts +60 -0
  212. package/dist/comments/routes.d.ts.map +1 -0
  213. package/dist/comments/routes.js +212 -0
  214. package/dist/comments/routes.js.map +1 -0
  215. package/dist/comments/submission.d.ts +297 -0
  216. package/dist/comments/submission.d.ts.map +1 -0
  217. package/dist/comments/submission.js +280 -0
  218. package/dist/comments/submission.js.map +1 -0
  219. package/dist/comments/viewer.d.ts +21 -0
  220. package/dist/comments/viewer.d.ts.map +1 -0
  221. package/dist/comments/viewer.js +45 -0
  222. package/dist/comments/viewer.js.map +1 -0
  223. package/dist/config.d.ts +225 -5
  224. package/dist/config.d.ts.map +1 -1
  225. package/dist/config.js +121 -6
  226. package/dist/config.js.map +1 -1
  227. package/dist/contact/delivery.d.ts +60 -0
  228. package/dist/contact/delivery.d.ts.map +1 -0
  229. package/dist/contact/delivery.js +87 -0
  230. package/dist/contact/delivery.js.map +1 -0
  231. package/dist/contact/form.d.ts +130 -0
  232. package/dist/contact/form.d.ts.map +1 -0
  233. package/dist/contact/form.js +163 -0
  234. package/dist/contact/form.js.map +1 -0
  235. package/dist/contact/index.d.ts +17 -0
  236. package/dist/contact/index.d.ts.map +1 -0
  237. package/dist/contact/index.js +13 -0
  238. package/dist/contact/index.js.map +1 -0
  239. package/dist/contact/records.d.ts +124 -0
  240. package/dist/contact/records.d.ts.map +1 -0
  241. package/dist/contact/records.js +218 -0
  242. package/dist/contact/records.js.map +1 -0
  243. package/dist/contact/routes.d.ts +17 -0
  244. package/dist/contact/routes.d.ts.map +1 -0
  245. package/dist/contact/routes.js +127 -0
  246. package/dist/contact/routes.js.map +1 -0
  247. package/dist/contact/submission.d.ts +103 -0
  248. package/dist/contact/submission.d.ts.map +1 -0
  249. package/dist/contact/submission.js +166 -0
  250. package/dist/contact/submission.js.map +1 -0
  251. package/dist/content/document.d.ts +7 -2
  252. package/dist/content/document.d.ts.map +1 -1
  253. package/dist/content/document.js +1 -0
  254. package/dist/content/document.js.map +1 -1
  255. package/dist/content/index.d.ts +7 -2
  256. package/dist/content/index.d.ts.map +1 -1
  257. package/dist/content/index.js +4 -1
  258. package/dist/content/index.js.map +1 -1
  259. package/dist/content/markdown.d.ts.map +1 -1
  260. package/dist/content/markdown.js +24 -2
  261. package/dist/content/markdown.js.map +1 -1
  262. package/dist/content/parser.d.ts.map +1 -1
  263. package/dist/content/parser.js +11 -4
  264. package/dist/content/parser.js.map +1 -1
  265. package/dist/content/save.d.ts +5 -0
  266. package/dist/content/save.d.ts.map +1 -1
  267. package/dist/content/save.js +44 -1
  268. package/dist/content/save.js.map +1 -1
  269. package/dist/content/schedule.d.ts +125 -0
  270. package/dist/content/schedule.d.ts.map +1 -0
  271. package/dist/content/schedule.js +207 -0
  272. package/dist/content/schedule.js.map +1 -0
  273. package/dist/content/search.d.ts +64 -0
  274. package/dist/content/search.d.ts.map +1 -0
  275. package/dist/content/search.js +136 -0
  276. package/dist/content/search.js.map +1 -0
  277. package/dist/content/store.d.ts +220 -7
  278. package/dist/content/store.d.ts.map +1 -1
  279. package/dist/content/store.js +392 -67
  280. package/dist/content/store.js.map +1 -1
  281. package/dist/content/sync.d.ts +7 -1
  282. package/dist/content/sync.d.ts.map +1 -1
  283. package/dist/content/sync.js +18 -10
  284. package/dist/content/sync.js.map +1 -1
  285. package/dist/content/time.d.ts +56 -0
  286. package/dist/content/time.d.ts.map +1 -0
  287. package/dist/content/time.js +141 -0
  288. package/dist/content/time.js.map +1 -0
  289. package/dist/content/writer.d.ts.map +1 -1
  290. package/dist/content/writer.js +3 -0
  291. package/dist/content/writer.js.map +1 -1
  292. package/dist/env.d.ts +70 -5
  293. package/dist/env.d.ts.map +1 -1
  294. package/dist/federation/actor.d.ts +95 -41
  295. package/dist/federation/actor.d.ts.map +1 -1
  296. package/dist/federation/actor.js +180 -53
  297. package/dist/federation/actor.js.map +1 -1
  298. package/dist/federation/article.d.ts +44 -17
  299. package/dist/federation/article.d.ts.map +1 -1
  300. package/dist/federation/article.js +98 -46
  301. package/dist/federation/article.js.map +1 -1
  302. package/dist/federation/delivery.d.ts +63 -15
  303. package/dist/federation/delivery.d.ts.map +1 -1
  304. package/dist/federation/delivery.js +154 -80
  305. package/dist/federation/delivery.js.map +1 -1
  306. package/dist/federation/federation.d.ts +22 -17
  307. package/dist/federation/federation.d.ts.map +1 -1
  308. package/dist/federation/federation.js +98 -77
  309. package/dist/federation/federation.js.map +1 -1
  310. package/dist/federation/followers.d.ts +3 -2
  311. package/dist/federation/followers.d.ts.map +1 -1
  312. package/dist/federation/followers.js +6 -5
  313. package/dist/federation/followers.js.map +1 -1
  314. package/dist/federation/import-wordpress.d.ts +128 -0
  315. package/dist/federation/import-wordpress.d.ts.map +1 -0
  316. package/dist/federation/import-wordpress.js +426 -0
  317. package/dist/federation/import-wordpress.js.map +1 -0
  318. package/dist/federation/inbox.d.ts +36 -13
  319. package/dist/federation/inbox.d.ts.map +1 -1
  320. package/dist/federation/inbox.js +120 -49
  321. package/dist/federation/inbox.js.map +1 -1
  322. package/dist/federation/index.d.ts +22 -10
  323. package/dist/federation/index.d.ts.map +1 -1
  324. package/dist/federation/index.js +13 -8
  325. package/dist/federation/index.js.map +1 -1
  326. package/dist/federation/keys.d.ts +98 -20
  327. package/dist/federation/keys.d.ts.map +1 -1
  328. package/dist/federation/keys.js +269 -39
  329. package/dist/federation/keys.js.map +1 -1
  330. package/dist/federation/mount.d.ts +46 -10
  331. package/dist/federation/mount.d.ts.map +1 -1
  332. package/dist/federation/mount.js +300 -28
  333. package/dist/federation/mount.js.map +1 -1
  334. package/dist/federation/paths.d.ts +23 -35
  335. package/dist/federation/paths.d.ts.map +1 -1
  336. package/dist/federation/paths.js +26 -39
  337. package/dist/federation/paths.js.map +1 -1
  338. package/dist/federation/records.d.ts +223 -0
  339. package/dist/federation/records.d.ts.map +1 -0
  340. package/dist/federation/records.js +506 -0
  341. package/dist/federation/records.js.map +1 -0
  342. package/dist/federation/relays.d.ts +114 -0
  343. package/dist/federation/relays.d.ts.map +1 -0
  344. package/dist/federation/relays.js +290 -0
  345. package/dist/federation/relays.js.map +1 -0
  346. package/dist/federation/replies.d.ts +80 -0
  347. package/dist/federation/replies.d.ts.map +1 -0
  348. package/dist/federation/replies.js +166 -0
  349. package/dist/federation/replies.js.map +1 -0
  350. package/dist/federation/wordpress.d.ts +131 -0
  351. package/dist/federation/wordpress.d.ts.map +1 -0
  352. package/dist/federation/wordpress.js +341 -0
  353. package/dist/federation/wordpress.js.map +1 -0
  354. package/dist/files/atomic.d.ts +103 -0
  355. package/dist/files/atomic.d.ts.map +1 -0
  356. package/dist/files/atomic.js +150 -0
  357. package/dist/files/atomic.js.map +1 -0
  358. package/dist/files/index.d.ts +3 -0
  359. package/dist/files/index.d.ts.map +1 -0
  360. package/dist/files/index.js +2 -0
  361. package/dist/files/index.js.map +1 -0
  362. package/dist/forms/protection.d.ts +85 -0
  363. package/dist/forms/protection.d.ts.map +1 -0
  364. package/dist/forms/protection.js +116 -0
  365. package/dist/forms/protection.js.map +1 -0
  366. package/dist/images/icons.d.ts +59 -0
  367. package/dist/images/icons.d.ts.map +1 -0
  368. package/dist/images/icons.js +131 -0
  369. package/dist/images/icons.js.map +1 -0
  370. package/dist/images/index.d.ts +7 -0
  371. package/dist/images/index.d.ts.map +1 -0
  372. package/dist/images/index.js +4 -0
  373. package/dist/images/index.js.map +1 -0
  374. package/dist/images/markup.d.ts +47 -0
  375. package/dist/images/markup.d.ts.map +1 -0
  376. package/dist/images/markup.js +175 -0
  377. package/dist/images/markup.js.map +1 -0
  378. package/dist/images/paths.d.ts +34 -0
  379. package/dist/images/paths.d.ts.map +1 -0
  380. package/dist/images/paths.js +41 -0
  381. package/dist/images/paths.js.map +1 -0
  382. package/dist/images/variants.d.ts +104 -0
  383. package/dist/images/variants.d.ts.map +1 -0
  384. package/dist/images/variants.js +320 -0
  385. package/dist/images/variants.js.map +1 -0
  386. package/dist/index.d.ts +123 -19
  387. package/dist/index.d.ts.map +1 -1
  388. package/dist/index.js +344 -25
  389. package/dist/index.js.map +1 -1
  390. package/dist/init.d.ts +18 -0
  391. package/dist/init.d.ts.map +1 -1
  392. package/dist/init.js +34 -3
  393. package/dist/init.js.map +1 -1
  394. package/dist/mail/brevo.d.ts +35 -0
  395. package/dist/mail/brevo.d.ts.map +1 -0
  396. package/dist/mail/brevo.js +85 -0
  397. package/dist/mail/brevo.js.map +1 -0
  398. package/dist/mail/credentials.d.ts +62 -0
  399. package/dist/mail/credentials.d.ts.map +1 -0
  400. package/dist/mail/credentials.js +86 -0
  401. package/dist/mail/credentials.js.map +1 -0
  402. package/dist/mail/index.d.ts +15 -0
  403. package/dist/mail/index.d.ts.map +1 -0
  404. package/dist/mail/index.js +8 -0
  405. package/dist/mail/index.js.map +1 -0
  406. package/dist/mail/memory.d.ts +35 -0
  407. package/dist/mail/memory.d.ts.map +1 -0
  408. package/dist/mail/memory.js +29 -0
  409. package/dist/mail/memory.js.map +1 -0
  410. package/dist/mail/provider.d.ts +80 -0
  411. package/dist/mail/provider.d.ts.map +1 -0
  412. package/dist/mail/provider.js +23 -0
  413. package/dist/mail/provider.js.map +1 -0
  414. package/dist/mail/service.d.ts +156 -0
  415. package/dist/mail/service.d.ts.map +1 -0
  416. package/dist/mail/service.js +265 -0
  417. package/dist/mail/service.js.map +1 -0
  418. package/dist/mail/smtp.d.ts +39 -0
  419. package/dist/mail/smtp.d.ts.map +1 -0
  420. package/dist/mail/smtp.js +58 -0
  421. package/dist/mail/smtp.js.map +1 -0
  422. package/dist/mail/templates.d.ts +91 -0
  423. package/dist/mail/templates.d.ts.map +1 -0
  424. package/dist/mail/templates.js +69 -0
  425. package/dist/mail/templates.js.map +1 -0
  426. package/dist/notifications/comments.d.ts +66 -0
  427. package/dist/notifications/comments.d.ts.map +1 -0
  428. package/dist/notifications/comments.js +151 -0
  429. package/dist/notifications/comments.js.map +1 -0
  430. package/dist/notifications/digest.d.ts +135 -0
  431. package/dist/notifications/digest.d.ts.map +1 -0
  432. package/dist/notifications/digest.js +287 -0
  433. package/dist/notifications/digest.js.map +1 -0
  434. package/dist/notifications/index.d.ts +13 -0
  435. package/dist/notifications/index.d.ts.map +1 -0
  436. package/dist/notifications/index.js +8 -0
  437. package/dist/notifications/index.js.map +1 -0
  438. package/dist/notifications/links.d.ts +60 -0
  439. package/dist/notifications/links.d.ts.map +1 -0
  440. package/dist/notifications/links.js +63 -0
  441. package/dist/notifications/links.js.map +1 -0
  442. package/dist/notifications/optouts.d.ts +48 -0
  443. package/dist/notifications/optouts.d.ts.map +1 -0
  444. package/dist/notifications/optouts.js +93 -0
  445. package/dist/notifications/optouts.js.map +1 -0
  446. package/dist/notifications/preferences.d.ts +158 -0
  447. package/dist/notifications/preferences.d.ts.map +1 -0
  448. package/dist/notifications/preferences.js +195 -0
  449. package/dist/notifications/preferences.js.map +1 -0
  450. package/dist/notifications/routes.d.ts +28 -0
  451. package/dist/notifications/routes.d.ts.map +1 -0
  452. package/dist/notifications/routes.js +184 -0
  453. package/dist/notifications/routes.js.map +1 -0
  454. package/dist/notifications/tokens.d.ts +60 -0
  455. package/dist/notifications/tokens.d.ts.map +1 -0
  456. package/dist/notifications/tokens.js +125 -0
  457. package/dist/notifications/tokens.js.map +1 -0
  458. package/dist/notify.d.ts +84 -0
  459. package/dist/notify.d.ts.map +1 -0
  460. package/dist/notify.js +149 -0
  461. package/dist/notify.js.map +1 -0
  462. package/dist/web/archive.d.ts +49 -0
  463. package/dist/web/archive.d.ts.map +1 -0
  464. package/dist/web/archive.js +59 -0
  465. package/dist/web/archive.js.map +1 -0
  466. package/dist/web/assets.d.ts +6 -5
  467. package/dist/web/assets.d.ts.map +1 -1
  468. package/dist/web/assets.js +5 -7
  469. package/dist/web/assets.js.map +1 -1
  470. package/dist/web/authors.d.ts +143 -0
  471. package/dist/web/authors.d.ts.map +1 -0
  472. package/dist/web/authors.js +178 -0
  473. package/dist/web/authors.js.map +1 -0
  474. package/dist/web/comments.d.ts +60 -0
  475. package/dist/web/comments.d.ts.map +1 -0
  476. package/dist/web/comments.js +207 -0
  477. package/dist/web/comments.js.map +1 -0
  478. package/dist/web/context.d.ts +162 -28
  479. package/dist/web/context.d.ts.map +1 -1
  480. package/dist/web/context.js +123 -26
  481. package/dist/web/context.js.map +1 -1
  482. package/dist/web/conversation.d.ts +209 -0
  483. package/dist/web/conversation.d.ts.map +1 -0
  484. package/dist/web/conversation.js +557 -0
  485. package/dist/web/conversation.js.map +1 -0
  486. package/dist/web/documents.d.ts +36 -6
  487. package/dist/web/documents.d.ts.map +1 -1
  488. package/dist/web/documents.js +52 -10
  489. package/dist/web/documents.js.map +1 -1
  490. package/dist/web/feed-atom.d.ts +22 -0
  491. package/dist/web/feed-atom.d.ts.map +1 -0
  492. package/dist/web/feed-atom.js +83 -0
  493. package/dist/web/feed-atom.js.map +1 -0
  494. package/dist/web/feed-item.d.ts +145 -0
  495. package/dist/web/feed-item.d.ts.map +1 -0
  496. package/dist/web/feed-item.js +120 -0
  497. package/dist/web/feed-item.js.map +1 -0
  498. package/dist/web/feed-json.d.ts +79 -0
  499. package/dist/web/feed-json.d.ts.map +1 -0
  500. package/dist/web/feed-json.js +53 -0
  501. package/dist/web/feed-json.js.map +1 -0
  502. package/dist/web/feed-rss.d.ts +26 -0
  503. package/dist/web/feed-rss.d.ts.map +1 -0
  504. package/dist/web/feed-rss.js +203 -0
  505. package/dist/web/feed-rss.js.map +1 -0
  506. package/dist/web/feed-source.d.ts +244 -0
  507. package/dist/web/feed-source.d.ts.map +1 -0
  508. package/dist/web/feed-source.js +201 -0
  509. package/dist/web/feed-source.js.map +1 -0
  510. package/dist/web/feed-xml.d.ts +63 -0
  511. package/dist/web/feed-xml.d.ts.map +1 -0
  512. package/dist/web/feed-xml.js +116 -0
  513. package/dist/web/feed-xml.js.map +1 -0
  514. package/dist/web/feeds.d.ts +31 -96
  515. package/dist/web/feeds.d.ts.map +1 -1
  516. package/dist/web/feeds.js +94 -198
  517. package/dist/web/feeds.js.map +1 -1
  518. package/dist/web/health.d.ts +31 -0
  519. package/dist/web/health.d.ts.map +1 -0
  520. package/dist/web/health.js +44 -0
  521. package/dist/web/health.js.map +1 -0
  522. package/dist/web/index.d.ts +30 -10
  523. package/dist/web/index.d.ts.map +1 -1
  524. package/dist/web/index.js +18 -7
  525. package/dist/web/index.js.map +1 -1
  526. package/dist/web/navigation.d.ts +195 -0
  527. package/dist/web/navigation.d.ts.map +1 -0
  528. package/dist/web/navigation.js +300 -0
  529. package/dist/web/navigation.js.map +1 -0
  530. package/dist/web/negotiate.d.ts +31 -0
  531. package/dist/web/negotiate.d.ts.map +1 -1
  532. package/dist/web/negotiate.js +31 -11
  533. package/dist/web/negotiate.js.map +1 -1
  534. package/dist/web/recent.d.ts +43 -0
  535. package/dist/web/recent.d.ts.map +1 -0
  536. package/dist/web/recent.js +37 -0
  537. package/dist/web/recent.js.map +1 -0
  538. package/dist/web/render.d.ts +203 -9
  539. package/dist/web/render.d.ts.map +1 -1
  540. package/dist/web/render.js +341 -17
  541. package/dist/web/render.js.map +1 -1
  542. package/dist/web/routes.d.ts +25 -8
  543. package/dist/web/routes.d.ts.map +1 -1
  544. package/dist/web/routes.js +854 -93
  545. package/dist/web/routes.js.map +1 -1
  546. package/dist/web/sanitize.d.ts +19 -0
  547. package/dist/web/sanitize.d.ts.map +1 -0
  548. package/dist/web/sanitize.js +272 -0
  549. package/dist/web/sanitize.js.map +1 -0
  550. package/dist/web/search.d.ts +79 -0
  551. package/dist/web/search.d.ts.map +1 -0
  552. package/dist/web/search.js +94 -0
  553. package/dist/web/search.js.map +1 -0
  554. package/dist/web/sitemap.d.ts +120 -0
  555. package/dist/web/sitemap.d.ts.map +1 -0
  556. package/dist/web/sitemap.js +199 -0
  557. package/dist/web/sitemap.js.map +1 -0
  558. package/dist/web/taxonomy.d.ts +152 -0
  559. package/dist/web/taxonomy.d.ts.map +1 -0
  560. package/dist/web/taxonomy.js +237 -0
  561. package/dist/web/taxonomy.js.map +1 -0
  562. package/dist/web/templates.d.ts +54 -16
  563. package/dist/web/templates.d.ts.map +1 -1
  564. package/dist/web/templates.js +109 -28
  565. package/dist/web/templates.js.map +1 -1
  566. package/dist/web/themes.d.ts +226 -0
  567. package/dist/web/themes.d.ts.map +1 -0
  568. package/dist/web/themes.js +314 -0
  569. package/dist/web/themes.js.map +1 -0
  570. package/dist/webmention/discovery.d.ts +51 -0
  571. package/dist/webmention/discovery.d.ts.map +1 -0
  572. package/dist/webmention/discovery.js +176 -0
  573. package/dist/webmention/discovery.js.map +1 -0
  574. package/dist/webmention/html.d.ts +71 -0
  575. package/dist/webmention/html.d.ts.map +1 -0
  576. package/dist/webmention/html.js +285 -0
  577. package/dist/webmention/html.js.map +1 -0
  578. package/dist/webmention/index.d.ts +12 -0
  579. package/dist/webmention/index.d.ts.map +1 -0
  580. package/dist/webmention/index.js +8 -0
  581. package/dist/webmention/index.js.map +1 -0
  582. package/dist/webmention/links.d.ts +18 -0
  583. package/dist/webmention/links.d.ts.map +1 -0
  584. package/dist/webmention/links.js +64 -0
  585. package/dist/webmention/links.js.map +1 -0
  586. package/dist/webmention/microformats.d.ts +93 -0
  587. package/dist/webmention/microformats.d.ts.map +1 -0
  588. package/dist/webmention/microformats.js +376 -0
  589. package/dist/webmention/microformats.js.map +1 -0
  590. package/dist/webmention/receive.d.ts +141 -0
  591. package/dist/webmention/receive.d.ts.map +1 -0
  592. package/dist/webmention/receive.js +264 -0
  593. package/dist/webmention/receive.js.map +1 -0
  594. package/dist/webmention/routes.d.ts +37 -0
  595. package/dist/webmention/routes.d.ts.map +1 -0
  596. package/dist/webmention/routes.js +75 -0
  597. package/dist/webmention/routes.js.map +1 -0
  598. package/dist/webmention/service.d.ts +94 -0
  599. package/dist/webmention/service.d.ts.map +1 -0
  600. package/dist/webmention/service.js +207 -0
  601. package/dist/webmention/service.js.map +1 -0
  602. package/package.json +11 -6
  603. package/templates/site/content/_data/site.json +8 -0
  604. package/templates/site/content/pages/about.md +7 -0
  605. package/templates/site/content/posts/2026-01-01-hello-world.md +5 -3
  606. package/templates/site/geekity.config.ts +11 -1
  607. package/templates/site/tsconfig.json +1 -1
  608. package/themes/default/README.md +1216 -50
  609. package/themes/default/layouts/404.njk +2 -3
  610. package/themes/default/layouts/author.njk +36 -0
  611. package/themes/default/layouts/base.njk +161 -31
  612. package/themes/default/layouts/category.njk +27 -0
  613. package/themes/default/layouts/front-page.njk +56 -0
  614. package/themes/default/layouts/home.njk +17 -1
  615. package/themes/default/layouts/page.njk +57 -5
  616. package/themes/default/layouts/post.njk +78 -19
  617. package/themes/default/layouts/search.njk +57 -0
  618. package/themes/default/layouts/tag.njk +3 -6
  619. package/themes/default/mail/comment-digest.html.njk +37 -0
  620. package/themes/default/mail/comment-digest.subject.njk +3 -0
  621. package/themes/default/mail/comment-digest.txt.njk +40 -0
  622. package/themes/default/mail/comment-pending.html.njk +27 -0
  623. package/themes/default/mail/comment-pending.subject.njk +2 -0
  624. package/themes/default/mail/comment-pending.txt.njk +24 -0
  625. package/themes/default/mail/comment-reply.html.njk +21 -0
  626. package/themes/default/mail/comment-reply.subject.njk +2 -0
  627. package/themes/default/mail/comment-reply.txt.njk +22 -0
  628. package/themes/default/mail/contact-message.html.njk +24 -0
  629. package/themes/default/mail/contact-message.subject.njk +5 -0
  630. package/themes/default/mail/contact-message.txt.njk +20 -0
  631. package/themes/default/mail/password-changed.html.njk +18 -0
  632. package/themes/default/mail/password-changed.subject.njk +2 -0
  633. package/themes/default/mail/password-changed.txt.njk +17 -0
  634. package/themes/default/mail/password-reset.html.njk +22 -0
  635. package/themes/default/mail/password-reset.subject.njk +2 -0
  636. package/themes/default/mail/password-reset.txt.njk +18 -0
  637. package/themes/default/mail/test.html.njk +14 -0
  638. package/themes/default/mail/test.subject.njk +2 -0
  639. package/themes/default/mail/test.txt.njk +12 -0
  640. package/themes/default/partials/archive.njk +27 -0
  641. package/themes/default/partials/bio.njk +71 -0
  642. package/themes/default/partials/comment-form.njk +137 -0
  643. package/themes/default/partials/contact-form.njk +88 -0
  644. package/themes/default/partials/conversation.njk +139 -0
  645. package/themes/default/partials/feeds.njk +26 -0
  646. package/themes/default/partials/jsonld.njk +120 -0
  647. package/themes/default/partials/menu.njk +32 -0
  648. package/themes/default/partials/pagination.njk +12 -6
  649. package/themes/default/partials/post-list.njk +51 -18
  650. package/themes/default/partials/search-form.njk +22 -0
  651. package/themes/default/partials/tags.njk +26 -5
  652. package/themes/default/static/highlight.js +4 -0
  653. package/themes/default/static/style.css +1142 -194
  654. package/themes/default/theme.json +9 -0
  655. package/admin/layouts/federation.njk +0 -145
  656. package/admin/layouts/login.njk +0 -23
  657. package/admin/layouts/settings.njk +0 -116
  658. package/admin/layouts/setup.njk +0 -27
  659. package/admin/layouts/users.njk +0 -91
  660. /package/admin/{layouts/_flash.njk → components/flash.njk} +0 -0
@@ -0,0 +1,680 @@
1
+ import path from 'node:path';
2
+ import { readFileIfPresentSync, updateFileAtomically, writeFileAtomicallySync, } from '../files/atomic.js';
3
+ import { deliveryMode, notificationEvent, withNotification, withNotificationMode, } from '../notifications/preferences.js';
4
+ import { hashPassword, verifyPasswordHash } from './passwords.js';
5
+ /**
6
+ * Who may sign in: `data/users.json`, and everything that reads or writes it.
7
+ *
8
+ * decision-9 puts every piece of irreducible state in a file, and a login is
9
+ * as irreducible as state gets — an account cannot be derived from anything,
10
+ * and a site that lost its users would fall back into first-run setup with the
11
+ * door open to whoever reached it first. So the file is the truth and the
12
+ * database is a cache: `data/geekity.db` may be deleted at rest, and the next
13
+ * boot still knows exactly who may sign in.
14
+ *
15
+ * It lives in `data/` rather than `content/`, unlike `site.json`, because
16
+ * `content/` is published with the site and in git, and an argon2 hash is not
17
+ * something to hand a reader or a mirror. The file is written 0600 for the
18
+ * same reason; it sits directly in `dataDir`, so there is no private directory
19
+ * around it to lean on.
20
+ *
21
+ * Reads are synchronous and per call, exactly as `readSiteSettings` reads
22
+ * `site.json` per call: the file is a few hundred bytes and nothing anywhere
23
+ * can then be holding a stale copy of who exists. Writes go through
24
+ * {@link updateFileAtomically}, which puts the read inside the same queue as
25
+ * the write, so the uniqueness rule that used to be a `UNIQUE` index is
26
+ * checked and acted on as one step.
27
+ */
28
+ /** The file, relative to `dataDir`. */
29
+ export const USERS_FILE = 'users.json';
30
+ /** The permissions the file is written with: nobody but the site's own user. */
31
+ export const USERS_FILE_MODE = 0o600;
32
+ /** Where the users file lives for a given data directory. */
33
+ export function usersFile(dataDir) {
34
+ return path.join(dataDir, USERS_FILE);
35
+ }
36
+ /** Thrown when a username is already taken. Usernames are the login key. */
37
+ export class DuplicateUsernameError extends Error {
38
+ name = 'DuplicateUsernameError';
39
+ /** The name that was already in use. */
40
+ username;
41
+ constructor(username) {
42
+ super(`A user named "${username}" already exists.`);
43
+ this.username = username;
44
+ }
45
+ }
46
+ /**
47
+ * Every user, by name, without password hashes.
48
+ *
49
+ * Sorted by code unit rather than by locale, which is how the `ORDER BY
50
+ * username` over a `BINARY` column this replaces sorted them: a listing that
51
+ * changed order with the server's locale would be a listing nobody could
52
+ * describe.
53
+ */
54
+ export function listUsers(dataDir) {
55
+ return readUsersFile(dataDir)
56
+ .users.map(withoutHash)
57
+ .sort((one, other) => one.username < other.username ? -1 : one.username > other.username ? 1 : 0);
58
+ }
59
+ /**
60
+ * The site's first account: the lowest id, which is the one the first run
61
+ * made.
62
+ *
63
+ * decision-14 leaves the site with no actor of its own, and two things still
64
+ * have to be somebody's: a relay subscription, which is an instance-wide
65
+ * agreement rather than one person's, and a post whose `author` names nobody
66
+ * this site knows. Both fall to the first account, because a site's first
67
+ * account is the one that has always existed and the only one that can be
68
+ * chosen without asking. `undefined` before anybody has signed up, which is
69
+ * first-run setup and federates nothing.
70
+ */
71
+ export function primaryUser(dataDir) {
72
+ return readUsersFile(dataDir)
73
+ .users.map(withoutHash)
74
+ .reduce((lowest, user) => (lowest === undefined || user.id < lowest.id ? user : lowest), undefined);
75
+ }
76
+ /** How many users exist. Zero is what puts the admin into first-run setup. */
77
+ export function countUsers(dataDir) {
78
+ return readUsersFile(dataDir).users.length;
79
+ }
80
+ /**
81
+ * One user with its password hash, or `undefined`. For auth, not for screens.
82
+ */
83
+ export function findUser(dataDir, username) {
84
+ return readUsersFile(dataDir).users.find((user) => user.username === username);
85
+ }
86
+ /** The user behind a session's `userId`, or `undefined`. */
87
+ export function findUserById(dataDir, id) {
88
+ const found = readUsersFile(dataDir).users.find((user) => user.id === id);
89
+ return found === undefined ? undefined : withoutHash(found);
90
+ }
91
+ /**
92
+ * The user a forgot-password form named, by either of the two things somebody
93
+ * might type: their username or their email address.
94
+ *
95
+ * The username is compared exactly, because that is how logging in compares it
96
+ * and a second, looser rule would let one person's name match another's
97
+ * account. The email is folded to lower case on both sides, because an address
98
+ * is case-insensitive in practice and nobody remembers how they capitalised
99
+ * one. A user is matched by username first, so a site where somebody's address
100
+ * happens to be another person's username still resolves the way logging in
101
+ * would. The empty string matches nobody, however many users have no email.
102
+ */
103
+ export function findUserByIdentifier(dataDir, identifier) {
104
+ const wanted = identifier.trim();
105
+ if (wanted === '')
106
+ return undefined;
107
+ const { users } = readUsersFile(dataDir);
108
+ const folded = wanted.toLowerCase();
109
+ const found = users.find((user) => user.username === wanted) ??
110
+ users.find((user) => user.email !== undefined && user.email.toLowerCase() === folded);
111
+ return found === undefined ? undefined : withoutHash(found);
112
+ }
113
+ /**
114
+ * Hash the password and add the user to the file.
115
+ *
116
+ * Throws {@link DuplicateUsernameError} when the name is taken. The check is
117
+ * inside the write, not before it, which is what makes it the rule the
118
+ * `UNIQUE` index used to be: two requests adding the same name at the same
119
+ * moment queue behind one another on the file, so the second one reads the
120
+ * first one's work and is refused.
121
+ */
122
+ export async function createUser(input) {
123
+ const hash = hashPassword(input.password);
124
+ const email = (input.email ?? '').trim();
125
+ let created;
126
+ await write(input.dataDir, (contents) => {
127
+ if (contents.users.some((user) => user.username === input.username)) {
128
+ throw new DuplicateUsernameError(input.username);
129
+ }
130
+ created = {
131
+ id: contents.nextId,
132
+ username: input.username,
133
+ // Absent rather than empty, so a user with no email has no key for one
134
+ // and the file says only what somebody actually filled in.
135
+ ...(email === '' ? {} : { email }),
136
+ passwordHash: hash,
137
+ createdAt: new Date().toISOString(),
138
+ };
139
+ return { users: [...contents.users, created], nextId: contents.nextId + 1 };
140
+ });
141
+ // `write` either threw or ran the callback, so this is only unreachable.
142
+ if (created === undefined)
143
+ throw new Error('The user was not written.');
144
+ return withoutHash(created);
145
+ }
146
+ /**
147
+ * Hash a new password and put it on a user, replacing the old one. Returns
148
+ * `false` when there is no such user. Sessions are left alone; deciding which
149
+ * of them a password change should end is the caller's business.
150
+ */
151
+ export async function setUserPassword(input) {
152
+ // Hashed before the write rather than inside it, so a hash that throws
153
+ // leaves the stored one alone.
154
+ const hash = hashPassword(input.password);
155
+ let changed = false;
156
+ await write(input.dataDir, (contents) => {
157
+ changed = contents.users.some((user) => user.id === input.userId);
158
+ return {
159
+ ...contents,
160
+ users: contents.users.map((user) => user.id === input.userId ? { ...user, passwordHash: hash } : user),
161
+ };
162
+ });
163
+ return changed;
164
+ }
165
+ /**
166
+ * Put an email address on a user, or take the one they had off.
167
+ *
168
+ * The empty string is how an address is removed, so one form field can do
169
+ * both: a box somebody cleared means "I do not want to be emailed", and there
170
+ * is nothing else it could mean. Returns `false` when there is no such user.
171
+ * The address is stored as it was typed, capitals and all — it is what a
172
+ * message is addressed to, and only the matching is case-insensitive.
173
+ */
174
+ export async function setUserEmail(input) {
175
+ const email = input.email.trim();
176
+ let changed = false;
177
+ await write(input.dataDir, (contents) => {
178
+ changed = contents.users.some((user) => user.id === input.userId);
179
+ return {
180
+ ...contents,
181
+ users: contents.users.map((user) => {
182
+ if (user.id !== input.userId)
183
+ return user;
184
+ const { email: _removed, ...rest } = user;
185
+ return email === '' ? rest : { ...rest, email };
186
+ }),
187
+ };
188
+ });
189
+ return changed;
190
+ }
191
+ /**
192
+ * Write a user's profile, replacing whatever was there.
193
+ *
194
+ * The whole profile at once rather than a field at a time, because that is
195
+ * what the form on the users screen submits: four boxes and a Save, and a
196
+ * field somebody cleared is a field they no longer want. {@link cleanProfile}
197
+ * drops the empties, so a profile with nothing left in it leaves the user with
198
+ * no `profile` key rather than with an empty object. Returns `false` when
199
+ * there is no such user.
200
+ */
201
+ export async function setUserProfile(input) {
202
+ const next = cleanProfile(input.profile);
203
+ let changed = false;
204
+ await write(input.dataDir, (contents) => {
205
+ changed = contents.users.some((user) => user.id === input.userId);
206
+ return {
207
+ ...contents,
208
+ users: contents.users.map((user) => {
209
+ if (user.id !== input.userId)
210
+ return user;
211
+ const { profile: _removed, ...rest } = user;
212
+ return next === undefined ? rest : { ...rest, profile: next };
213
+ }),
214
+ };
215
+ });
216
+ return changed;
217
+ }
218
+ /**
219
+ * A profile with the empties taken out, or `undefined` when nothing is left.
220
+ *
221
+ * Trimming here rather than at the form is what makes a hand-edited file and a
222
+ * saved one read the same: a bio of three spaces is a bio nobody wrote, and a
223
+ * link with no URL is not somewhere this person is.
224
+ */
225
+ export function cleanProfile(profile) {
226
+ const displayName = (profile.displayName ?? '').trim();
227
+ const bio = (profile.bio ?? '').trim();
228
+ const avatar = (profile.avatar ?? '').trim();
229
+ const jobTitle = (profile.jobTitle ?? '').trim();
230
+ const location = (profile.location ?? '').trim();
231
+ const links = (profile.links ?? [])
232
+ .map((link) => ({ label: link.label.trim(), href: link.href.trim() }))
233
+ // A link needs somewhere to go; a label it does not have is the URL again,
234
+ // because a list of blank links is worse than a list of bare addresses.
235
+ .filter((link) => link.href !== '')
236
+ .map((link) => (link.label === '' ? { label: link.href, href: link.href } : link));
237
+ const cleaned = {
238
+ ...(displayName === '' ? {} : { displayName }),
239
+ ...(bio === '' ? {} : { bio }),
240
+ ...(avatar === '' ? {} : { avatar }),
241
+ ...(jobTitle === '' ? {} : { jobTitle }),
242
+ ...(location === '' ? {} : { location }),
243
+ ...(links.length === 0 ? {} : { links }),
244
+ };
245
+ return Object.keys(cleaned).length === 0 ? undefined : cleaned;
246
+ }
247
+ /**
248
+ * Turn one notice on or off for one user.
249
+ *
250
+ * The map holds only what somebody changed, so this writes through
251
+ * {@link withNotification} and drops the key altogether when the answer is the
252
+ * default again. An event this version has never heard of changes nothing:
253
+ * there would be no switch to reach it with and no sender to read it. Returns
254
+ * `false` when there is no such user.
255
+ */
256
+ export async function setUserNotification(input) {
257
+ let changed = false;
258
+ await write(input.dataDir, (contents) => {
259
+ changed = contents.users.some((user) => user.id === input.userId);
260
+ return {
261
+ ...contents,
262
+ users: contents.users.map((user) => {
263
+ if (user.id !== input.userId)
264
+ return user;
265
+ const { notifications: _removed, ...rest } = user;
266
+ const next = withNotification(user.notifications, input.event, input.on);
267
+ return next === undefined ? rest : { ...rest, notifications: next };
268
+ }),
269
+ };
270
+ });
271
+ return changed;
272
+ }
273
+ /**
274
+ * Say how often one notice reaches one user.
275
+ *
276
+ * The twin of {@link setUserNotification}, and it writes on the same rule:
277
+ * through {@link withNotificationMode}, which drops the key when the answer is
278
+ * the default again, when the mode is one this version does not know, and when
279
+ * the event does not offer a choice at all. So a form submitting nonsense
280
+ * leaves the file saying nothing rather than saying something unreachable.
281
+ * Returns `false` when there is no such user.
282
+ */
283
+ export async function setUserNotificationMode(input) {
284
+ let changed = false;
285
+ await write(input.dataDir, (contents) => {
286
+ changed = contents.users.some((user) => user.id === input.userId);
287
+ return {
288
+ ...contents,
289
+ users: contents.users.map((user) => {
290
+ if (user.id !== input.userId)
291
+ return user;
292
+ const { notificationModes: _removed, ...rest } = user;
293
+ const next = withNotificationMode(user.notificationModes, input.event, input.mode);
294
+ return next === undefined ? rest : { ...rest, notificationModes: next };
295
+ }),
296
+ };
297
+ });
298
+ return changed;
299
+ }
300
+ /**
301
+ * Thrown when the id being imported already belongs to somebody else.
302
+ *
303
+ * Two accounts answering to one actor id is two people with one identity, and
304
+ * two carrying one WordPress number is a delivery to the plugin's old inbox
305
+ * that could go to either. Neither is something to pick a winner for, so the
306
+ * import stops and says whose it is.
307
+ */
308
+ export class ConflictingActorIdError extends Error {
309
+ name = 'ConflictingActorIdError';
310
+ /** The user who already has it. */
311
+ username;
312
+ constructor(username, what) {
313
+ super(`${what} already belongs to "${username}".`);
314
+ this.username = username;
315
+ }
316
+ }
317
+ /**
318
+ * Write the two ids a site arriving from the WordPress ActivityPub plugin
319
+ * carries: the actor id its followers hold, and the number its paths are built
320
+ * from (TASK-69, TASK-70).
321
+ *
322
+ * The only writer of either field, and it is not a screen: doc-4 makes a
323
+ * stored actor id identity for the life of the account, and identity is not
324
+ * something a form should be able to retype. `geekity import wordpress-actor`
325
+ * is what calls this.
326
+ *
327
+ * Both ids have to be unique across the file, and the check is inside the
328
+ * write for the reason {@link createUser}'s duplicate check is: it is a rule
329
+ * about the file rather than about one user, so it is read and acted on as one
330
+ * step. Returns whether anything changed — a second run with the same ids
331
+ * writes nothing, which is what makes the import idempotent.
332
+ */
333
+ export async function setUserWordPressActor(input) {
334
+ let changed = false;
335
+ await write(input.dataDir, (contents) => {
336
+ const mine = contents.users.find((user) => user.username === input.username);
337
+ if (mine === undefined)
338
+ throw new UnknownUserError(input.username);
339
+ const byActorId = contents.users.find((user) => user.id !== mine.id && user.actorId === input.actorId);
340
+ if (byActorId !== undefined) {
341
+ throw new ConflictingActorIdError(byActorId.username, `The actor id ${input.actorId}`);
342
+ }
343
+ const byNumber = contents.users.find((user) => user.id !== mine.id && user.wordpressActorId === input.wordpressActorId);
344
+ if (byNumber !== undefined) {
345
+ throw new ConflictingActorIdError(byNumber.username, `WordPress actor ${String(input.wordpressActorId)}`);
346
+ }
347
+ changed = mine.actorId !== input.actorId || mine.wordpressActorId !== input.wordpressActorId;
348
+ if (!changed)
349
+ return contents;
350
+ return {
351
+ ...contents,
352
+ users: contents.users.map((user) => user.id === mine.id
353
+ ? { ...user, actorId: input.actorId, wordpressActorId: input.wordpressActorId }
354
+ : user),
355
+ };
356
+ });
357
+ return changed;
358
+ }
359
+ /** Thrown when a command names a user this site does not have. */
360
+ export class UnknownUserError extends Error {
361
+ name = 'UnknownUserError';
362
+ /** The name that was asked for. */
363
+ username;
364
+ constructor(username) {
365
+ super(`This site has no user named "${username}".`);
366
+ this.username = username;
367
+ }
368
+ }
369
+ /**
370
+ * Take a user out of the file. Returns `false` when there was nothing to take.
371
+ *
372
+ * Their sessions are not ended here: sessions are in the database, and the
373
+ * screen that deletes a user ends them itself. A session that outlives this —
374
+ * one in a database restored from a backup, say — names a user the file no
375
+ * longer holds, and the admin guard turns that into no session at all.
376
+ */
377
+ export async function deleteUser(input) {
378
+ let deleted = false;
379
+ await write(input.dataDir, (contents) => {
380
+ deleted = contents.users.some((user) => user.id === input.userId);
381
+ return { ...contents, users: contents.users.filter((user) => user.id !== input.userId) };
382
+ });
383
+ return deleted;
384
+ }
385
+ /**
386
+ * The user, when the password is theirs; `undefined` when it is not, or when
387
+ * there is no such user. The two failures are deliberately indistinguishable.
388
+ */
389
+ export function verifyUserPassword(dataDir, username, password) {
390
+ const user = findUser(dataDir, username);
391
+ if (user === undefined) {
392
+ // Hash anyway, so a missing user and a wrong password take the same time
393
+ // and the login form does not become a user enumerator.
394
+ verifyPasswordHash(DUMMY_HASH, password);
395
+ return undefined;
396
+ }
397
+ if (!verifyPasswordHash(user.passwordHash, password))
398
+ return undefined;
399
+ return withoutHash(user);
400
+ }
401
+ /**
402
+ * Turn an older site's users rows into `data/users.json`, once, and drop the
403
+ * table.
404
+ *
405
+ * TASK-9 put the accounts in SQLite; decision-9 made the database disposable,
406
+ * and a site upgrading across the two has rows nothing would ever read again.
407
+ * So the first boot of this version writes them out — ids and hashes exactly
408
+ * as they stand, because a session names its user by id and a hash is not
409
+ * something to re-encode on the way past.
410
+ *
411
+ * The file wins, as it does everywhere else: a `users.json` that is already
412
+ * there is left alone, so a database restored from a backup taken before the
413
+ * upgrade cannot bring deleted accounts back or undo a password change. The
414
+ * table is dropped either way, because a fresh database gets it from migration
415
+ * 1 — which has shipped, and so is never edited — and a site that has been
416
+ * migrated must not be asked again.
417
+ *
418
+ * Synchronous because `createCms` is: it runs before the server is listening
419
+ * and before anything else has touched the file.
420
+ */
421
+ export function migrateUsersToFile(options) {
422
+ const { admin, dataDir } = options;
423
+ const rows = admin.legacyUsers();
424
+ const file = usersFile(dataDir);
425
+ if (rows !== undefined && rows.length > 0 && readFileIfPresentSync(file) === undefined) {
426
+ const users = rows.map((row) => ({
427
+ id: row.id,
428
+ username: row.username,
429
+ passwordHash: row.passwordHash,
430
+ createdAt: row.createdAt,
431
+ }));
432
+ const highest = users.reduce((top, user) => Math.max(top, user.id), 0);
433
+ writeFileAtomicallySync(file, usersJsonText({ users, nextId: highest + 1 }), {
434
+ mode: USERS_FILE_MODE,
435
+ });
436
+ }
437
+ admin.dropLegacyTable('users');
438
+ }
439
+ /** A user as a screen may render it: everything but the hash. */
440
+ function withoutHash(user) {
441
+ return {
442
+ id: user.id,
443
+ username: user.username,
444
+ ...(user.email === undefined ? {} : { email: user.email }),
445
+ ...(user.notifications === undefined ? {} : { notifications: user.notifications }),
446
+ ...(user.notificationModes === undefined ? {} : { notificationModes: user.notificationModes }),
447
+ ...(user.profile === undefined ? {} : { profile: user.profile }),
448
+ ...(user.actorId === undefined ? {} : { actorId: user.actorId }),
449
+ ...(user.wordpressActorId === undefined ? {} : { wordpressActorId: user.wordpressActorId }),
450
+ createdAt: user.createdAt,
451
+ };
452
+ }
453
+ /**
454
+ * A hash of a password no user has, so a login attempt for an unknown
455
+ * username costs the same as one for a known name with the wrong password.
456
+ */
457
+ const DUMMY_HASH = hashPassword('a password no user has, hashed once so that verification is constant time');
458
+ /** The bytes of the file, as it is written. */
459
+ function usersJsonText(contents) {
460
+ return `${JSON.stringify(contents, null, 2)}\n`;
461
+ }
462
+ /**
463
+ * The file's contents, or an empty set of users when it is not there.
464
+ *
465
+ * A file that is there and will not parse throws rather than reading as empty,
466
+ * and that is the whole point of the strictness: no users means first-run
467
+ * setup, where the next person to reach `/admin` becomes the site's admin. A
468
+ * damaged file has to be a loud failure, never an open door.
469
+ */
470
+ function readUsersFile(dataDir) {
471
+ const file = usersFile(dataDir);
472
+ return parseUsersFile(readFileIfPresentSync(file), file);
473
+ }
474
+ /** The same, from bytes already in hand: `undefined` is a file that is not there. */
475
+ function parseUsersFile(source, file) {
476
+ if (source === undefined)
477
+ return { users: [], nextId: 1 };
478
+ let parsed;
479
+ try {
480
+ parsed = JSON.parse(source);
481
+ }
482
+ catch (error) {
483
+ throw unusableUsersFile(file, error);
484
+ }
485
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
486
+ throw unusableUsersFile(file, new Error('It is not a JSON object.'));
487
+ }
488
+ const listed = parsed['users'];
489
+ if (!Array.isArray(listed)) {
490
+ throw unusableUsersFile(file, new Error('It has no "users" array.'));
491
+ }
492
+ const users = listed.map((entry, index) => userFrom(entry, index, file));
493
+ const stated = parsed['nextId'];
494
+ const highest = users.reduce((top, user) => Math.max(top, user.id), 0);
495
+ // The stated `nextId` is the rule, but never one that would collide: a file
496
+ // edited by hand, or one written before this field existed, still hands out
497
+ // an id nobody has.
498
+ return {
499
+ users,
500
+ nextId: Math.max(typeof stated === 'number' && stated > 0 ? Math.trunc(stated) : 1, highest + 1),
501
+ };
502
+ }
503
+ /** One entry of the file as a user, or a refusal naming which entry is wrong. */
504
+ function userFrom(entry, index, file) {
505
+ const where = `Entry ${String(index + 1)} of "users"`;
506
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
507
+ throw unusableUsersFile(file, new Error(`${where} is not an object.`));
508
+ }
509
+ const record = entry;
510
+ const id = record['id'];
511
+ const username = record['username'];
512
+ const email = record['email'];
513
+ const preferences = notificationsFrom(record['notifications']);
514
+ const modes = notificationModesFrom(record['notificationModes']);
515
+ const profile = profileFrom(record['profile']);
516
+ const actorId = storedActorIdFrom(record['actorId']);
517
+ const wordpressActorId = wordpressActorIdFrom(record['wordpressActorId']);
518
+ const passwordHash = record['passwordHash'];
519
+ const createdAt = record['createdAt'];
520
+ if (typeof id !== 'number' || !Number.isInteger(id) || id <= 0) {
521
+ throw unusableUsersFile(file, new Error(`${where} has no id.`));
522
+ }
523
+ if (typeof username !== 'string' || username === '') {
524
+ throw unusableUsersFile(file, new Error(`${where} has no username.`));
525
+ }
526
+ if (typeof passwordHash !== 'string' || passwordHash === '') {
527
+ throw unusableUsersFile(file, new Error(`${where} has no password hash.`));
528
+ }
529
+ return {
530
+ id,
531
+ username,
532
+ // Unlike the three above, a bad email is dropped rather than refused: it
533
+ // is not what anybody signs in with, and a site whose whole admin refused
534
+ // to load over a mistyped address would be a worse failure than the one it
535
+ // is guarding against.
536
+ ...(typeof email === 'string' && email.trim() !== '' ? { email: email.trim() } : {}),
537
+ // Dropped rather than refused for the reason a bad email is: a preference
538
+ // is not what anybody signs in with, and an admin that would not load over
539
+ // a mistyped one would be the worse failure. A key naming an event this
540
+ // version does not know is left out too, so a file written by a newer
541
+ // version is read as far as it makes sense.
542
+ ...(preferences === undefined ? {} : { notifications: preferences }),
543
+ // Dropped on the same rule, and once more for a mode this version does not
544
+ // know: a stored `weekly` from a later version is read as the default
545
+ // rather than as a window nothing here could wait for.
546
+ ...(modes === undefined ? {} : { notificationModes: modes }),
547
+ // Dropped field by field on the same rule, and for the sharper reason that
548
+ // a profile is prose somebody may well have typed straight into the file:
549
+ // a bio that is a number is not a bio, and refusing to load the users file
550
+ // over one would take the whole admin down.
551
+ ...(profile === undefined ? {} : { profile }),
552
+ // Dropped rather than refused on the same rule again, and one more of its
553
+ // own: an id that is not a URL could never be requested, so keeping it
554
+ // would change nothing except to make a person's actor unreadable.
555
+ ...(actorId === undefined ? {} : { actorId }),
556
+ // And once more: a WordPress id that is not a whole positive number could
557
+ // never appear in one of the plugin's paths, so keeping it would only make
558
+ // the compatibility switch answer for a route nobody asks for.
559
+ ...(wordpressActorId === undefined ? {} : { wordpressActorId }),
560
+ passwordHash,
561
+ createdAt: typeof createdAt === 'string' ? createdAt : '',
562
+ };
563
+ }
564
+ /**
565
+ * A stored profile as this version reads it, or `undefined` when it says
566
+ * nothing usable.
567
+ *
568
+ * Read through the same {@link cleanProfile} a save goes through, so a file
569
+ * written by hand and one written by the users screen are read identically:
570
+ * whitespace is trimmed, an empty field is no field, and a link with no URL is
571
+ * no link.
572
+ */
573
+ function profileFrom(value) {
574
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
575
+ return undefined;
576
+ const record = value;
577
+ return cleanProfile({
578
+ ...optionalText('displayName', record['displayName']),
579
+ ...optionalText('bio', record['bio']),
580
+ ...optionalText('avatar', record['avatar']),
581
+ ...optionalText('jobTitle', record['jobTitle']),
582
+ ...optionalText('location', record['location']),
583
+ links: linksFrom(record['links']),
584
+ });
585
+ }
586
+ /**
587
+ * A stored actor id as this version reads it, or `undefined` when the file
588
+ * says nothing usable.
589
+ *
590
+ * It has to be an absolute `http`/`https` URL, because that is the only kind
591
+ * of thing a peer can dereference and the only kind the request middleware
592
+ * could ever match: a relative path or a bare word is not an id anybody holds.
593
+ * Anything else is dropped rather than refused, so a mistyped line leaves one
594
+ * person served under their author URL instead of taking the whole admin down.
595
+ */
596
+ function storedActorIdFrom(value) {
597
+ if (typeof value !== 'string')
598
+ return undefined;
599
+ const wanted = value.trim();
600
+ if (wanted === '')
601
+ return undefined;
602
+ try {
603
+ const url = new URL(wanted);
604
+ return url.protocol === 'http:' || url.protocol === 'https:' ? wanted : undefined;
605
+ }
606
+ catch {
607
+ return undefined;
608
+ }
609
+ }
610
+ /**
611
+ * A stored WordPress actor id as this version reads it, or `undefined`.
612
+ *
613
+ * A whole number above zero, because that is what a WordPress user id is and
614
+ * the only thing that could ever appear in one of the plugin's paths. A string
615
+ * is not accepted even when it reads as a number: the file is written by the
616
+ * import, and a quoted id would be a sign that something else wrote it.
617
+ */
618
+ function wordpressActorIdFrom(value) {
619
+ if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0)
620
+ return undefined;
621
+ return value;
622
+ }
623
+ /** `{ [key]: value }` when the value is a string, and nothing when it is not. */
624
+ function optionalText(key, value) {
625
+ return typeof value === 'string' ? { [key]: value } : {};
626
+ }
627
+ /** A stored list of profile links as this version reads it. */
628
+ function linksFrom(value) {
629
+ if (!Array.isArray(value))
630
+ return [];
631
+ const links = [];
632
+ for (const entry of value) {
633
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry))
634
+ continue;
635
+ const record = entry;
636
+ const href = record['href'];
637
+ const label = record['label'];
638
+ if (typeof href !== 'string')
639
+ continue;
640
+ links.push({ label: typeof label === 'string' ? label : '', href });
641
+ }
642
+ return links;
643
+ }
644
+ /** A stored preference map as this version reads it, or `undefined`. */
645
+ function notificationsFrom(value) {
646
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
647
+ return undefined;
648
+ const found = {};
649
+ for (const [name, wanted] of Object.entries(value)) {
650
+ if (typeof wanted === 'boolean' && notificationEvent(name) !== undefined)
651
+ found[name] = wanted;
652
+ }
653
+ return Object.keys(found).length === 0 ? undefined : found;
654
+ }
655
+ /** A stored map of delivery modes as this version reads it, or `undefined`. */
656
+ function notificationModesFrom(value) {
657
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
658
+ return undefined;
659
+ const found = {};
660
+ for (const [name, mode] of Object.entries(value)) {
661
+ const event = notificationEvent(name);
662
+ const known = deliveryMode(mode);
663
+ if (event?.batched === true && known !== undefined)
664
+ found[name] = known;
665
+ }
666
+ return Object.keys(found).length === 0 ? undefined : found;
667
+ }
668
+ /** What a damaged users file says, and what to do about it. */
669
+ function unusableUsersFile(file, cause) {
670
+ const reason = cause instanceof Error ? cause.message : String(cause);
671
+ return new Error(`The users file ${file} could not be read: ${reason} It has deliberately not been ` +
672
+ 'replaced, because a site with no users offers first-run setup to whoever reaches it ' +
673
+ 'first. Restore the file from a backup, or delete it on purpose to start again.');
674
+ }
675
+ /** Read the file, decide what it should say next, and write it, as one step. */
676
+ function write(dataDir, produce) {
677
+ const file = usersFile(dataDir);
678
+ return updateFileAtomically(file, (current) => usersJsonText(produce(parseUsersFile(current, file))), { mode: USERS_FILE_MODE });
679
+ }
680
+ //# sourceMappingURL=accounts.js.map