@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,150 @@
1
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ /** Every path with a write queued, keyed by its resolved absolute path. */
5
+ const queues = new Map();
6
+ /**
7
+ * Write a file atomically, behind whatever is already being written to it.
8
+ *
9
+ * The directory it lives in is created if it is missing, so a caller writing
10
+ * the first file under `data/keys` or `content/_data` does not have to.
11
+ */
12
+ export function writeFileAtomically(file, contents, options = {}) {
13
+ return enqueue(file, () => write(file, contents, options));
14
+ }
15
+ /**
16
+ * Read a file, decide what it should say next, and write it — all three as one
17
+ * step nothing else writing that file can get between.
18
+ *
19
+ * The read-modify-write is the shape almost every file in this CMS is changed
20
+ * by: `site.json` keeps the keys the settings form does not manage,
21
+ * `followers.json` gains or loses one entry, a month's inbox log gains a line.
22
+ * A queue around the write alone would not be enough for any of them, because
23
+ * two writers could both read the old file first.
24
+ */
25
+ export function updateFileAtomically(file, produce, options = {}) {
26
+ return withFileLock(file, async () => {
27
+ let current;
28
+ try {
29
+ current = await readFile(file, 'utf8');
30
+ }
31
+ catch {
32
+ current = undefined;
33
+ }
34
+ await write(file, await produce(current), options);
35
+ });
36
+ }
37
+ /**
38
+ * Run a task with one path's queue to itself, and hand back what it returned.
39
+ *
40
+ * The queue {@link updateFileAtomically} keeps around a read and a write, for
41
+ * a caller whose step is more than those two. The one that needs it is a file
42
+ * that SQLite indexes: a follower arriving has to reach `followers.json` and
43
+ * the `followers` table, and if the second of those were a step after the lock
44
+ * rather than inside it, a follow and an unfollow racing could leave the index
45
+ * saying something the file does not — and the file is the truth, so the index
46
+ * would be wrong until the next boot rebuilt it.
47
+ *
48
+ * The task is not handed a writer, because the writer it wants is the one that
49
+ * does not queue: {@link readFileIfPresentSync} and
50
+ * {@link writeFileAtomicallySync} inside the lock are the whole pattern.
51
+ * Calling {@link writeFileAtomically} or {@link updateFileAtomically} on the
52
+ * same path from inside a task would wait for a queue the task is itself at
53
+ * the head of, and never return.
54
+ */
55
+ export function withFileLock(file, task) {
56
+ return enqueue(file, async () => await task());
57
+ }
58
+ /**
59
+ * The same write for a caller that cannot await: boot.
60
+ *
61
+ * `createCms` is synchronous — a site calls it and reads `cms.app` — so the
62
+ * once-only migrations that turn an old database's rows into files have no
63
+ * `await` to spend. They run before the server is listening and before
64
+ * anything else has written a file, so stepping outside the queue costs
65
+ * nothing; the rename still makes the write atomic against a reader.
66
+ */
67
+ export function writeFileAtomicallySync(file, contents, options = {}) {
68
+ mkdirSync(path.dirname(file), { recursive: true });
69
+ const temporary = temporaryName(file);
70
+ try {
71
+ writeFileSync(temporary, contents, options.mode === undefined ? {} : { mode: options.mode });
72
+ renameSync(temporary, file);
73
+ }
74
+ catch (error) {
75
+ try {
76
+ unlinkSync(temporary);
77
+ }
78
+ catch {
79
+ // It was never created, or the rename already moved it away.
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+ /**
85
+ * Read a file's whole contents as text, or `undefined` when it is not there.
86
+ *
87
+ * The counterpart of the writes above, for the same reason they exist: a file
88
+ * that is the source of truth is read on the way to being written, and a
89
+ * missing one means "nothing stored yet" everywhere in this CMS rather than an
90
+ * error to handle at each call site.
91
+ */
92
+ export function readFileIfPresentSync(file) {
93
+ try {
94
+ return readFileSync(file, 'utf8');
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ /** Run a task once every task already queued for this path has finished. */
101
+ function enqueue(file, task) {
102
+ const key = path.resolve(file);
103
+ const queue = queues.get(key) ?? { last: Promise.resolve(), waiting: 0 };
104
+ queue.waiting += 1;
105
+ queues.set(key, queue);
106
+ // The chain is of settled promises: a task that throws still lets the next
107
+ // one run, and its rejection reaches only the caller that queued it.
108
+ const next = queue.last.then(task, task);
109
+ queue.last = next.catch(() => undefined);
110
+ return next.finally(() => {
111
+ // The last one out turns the light off, so a process that has been running
112
+ // for a year does not hold an entry per file it has ever written.
113
+ queue.waiting -= 1;
114
+ if (queue.waiting === 0 && queues.get(key) === queue)
115
+ queues.delete(key);
116
+ });
117
+ }
118
+ /** The write itself: a temporary sibling, then a rename over the target. */
119
+ async function write(file, contents, options) {
120
+ await mkdir(path.dirname(file), { recursive: true });
121
+ const temporary = temporaryName(file);
122
+ try {
123
+ await writeFile(temporary, contents, options.mode === undefined ? {} : { mode: options.mode });
124
+ await rename(temporary, file);
125
+ }
126
+ catch (error) {
127
+ try {
128
+ await unlink(temporary);
129
+ }
130
+ catch {
131
+ // It was never created, or the rename already moved it away.
132
+ }
133
+ throw error;
134
+ }
135
+ }
136
+ /** How many temporary names this process has minted. */
137
+ let sequence = 0;
138
+ /**
139
+ * A temporary name beside the file, unique to this write.
140
+ *
141
+ * Beside it rather than in the system temporary directory because `rename(2)`
142
+ * is only atomic within one filesystem. The process id keeps two processes on
143
+ * one site apart and the counter keeps two writes in one process apart, so a
144
+ * failed write's leftovers can never be mistaken for another's.
145
+ */
146
+ function temporaryName(file) {
147
+ sequence += 1;
148
+ return `${file}.${process.pid.toString(36)}-${sequence.toString(36)}.tmp`;
149
+ }
150
+ //# sourceMappingURL=atomic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic.js","sourceRoot":"","sources":["../../src/files/atomic.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { readFileIfPresentSync, updateFileAtomically, withFileLock, writeFileAtomically, writeFileAtomicallySync, } from './atomic.ts';
2
+ export type { FileContents, ProduceFileContents, WriteFileAtomicallyOptions } from './atomic.ts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/files/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,YAAY,EACZ,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { readFileIfPresentSync, updateFileAtomically, withFileLock, writeFileAtomically, writeFileAtomicallySync, } from './atomic.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/files/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,YAAY,EACZ,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * What every form on the public site defends itself with.
3
+ *
4
+ * A comment form (TASK-50) and a contact form (TASK-56) are the same problem
5
+ * wearing different fields: an anonymous stranger posting text at a site that
6
+ * has to store it. So the three cheap defences in front of both live here
7
+ * rather than once each, and neither feature can quietly drift into a weaker
8
+ * version of the other's rules.
9
+ *
10
+ * 1. **The honeypot.** A field no person can see and no person fills in. A
11
+ * submission that filled it is dropped without a word, because telling a bot
12
+ * why it failed is telling it how to succeed.
13
+ * 2. **The age of the form.** Something submitted a heartbeat after the page
14
+ * loaded was not typed, and something submitted from a form rendered last
15
+ * year is a replayed body. This is a speed bump rather than a control — the
16
+ * field is not signed, so anything that can read the form can forge it — and
17
+ * it is here because the naive half of the traffic does not bother.
18
+ * 3. **The address hash.** Not a defence in itself but what makes one
19
+ * reviewable: a salted hash of where a submission came from, so a run from
20
+ * one machine is visible without a reader's home address being written down.
21
+ *
22
+ * The fourth, the per-address rate limit, is `createLoginThrottle` in
23
+ * `admin/throttle.ts`, because it is the same limiter the login form uses and
24
+ * it belongs with the rest of that machinery.
25
+ */
26
+ /**
27
+ * The honeypot field, named as something a form-filling robot expects to find
28
+ * and a person never sees.
29
+ *
30
+ * The form hides it from sight and from assistive technology and carries
31
+ * `autocomplete="off"` on it, so a browser does not helpfully fill it in.
32
+ */
33
+ export declare const FORM_TRAP_FIELD = "website";
34
+ /** The hidden field carrying when the form was rendered, in epoch milliseconds. */
35
+ export declare const FORM_LOADED_FIELD = "loaded";
36
+ /** How long a form has to have been on screen before it may be submitted. */
37
+ export declare const MINIMUM_SUBMIT_SECONDS = 3;
38
+ /**
39
+ * How long a rendered form stays submittable.
40
+ *
41
+ * A day, so a page left open overnight still works, and a `loaded` from last
42
+ * year — which is what a replayed body looks like — does not.
43
+ */
44
+ export declare const MAXIMUM_FORM_AGE_SECONDS: number;
45
+ /** Whether the honeypot was filled in, which no person ever does. */
46
+ export declare function trapped(value: string): boolean;
47
+ /** How long ago the form was rendered, or `undefined` when it did not say. */
48
+ export declare function formAgeSeconds(loaded: string, now: Date): number | undefined;
49
+ /** What the clock says about a submission, when it says anything. */
50
+ export type FormTimingRefusal =
51
+ /** Rendered too long ago to still be the form it claims to be. */
52
+ 'stale'
53
+ /** Submitted faster than a person types. */
54
+ | 'too-quick';
55
+ /**
56
+ * Whether the form's age refuses this submission, and why.
57
+ *
58
+ * Staleness is decided before speed, because a `loaded` nothing can read is
59
+ * a forged field rather than a fast typist.
60
+ */
61
+ export declare function formTimingRefusal(loaded: string, now: Date): FormTimingRefusal | undefined;
62
+ /**
63
+ * Where the salt that hides submitters' addresses lives, under `dataDir`.
64
+ *
65
+ * Named after comments because comments minted it first and a site that has
66
+ * one must go on using it: a new salt would make every hash already in a
67
+ * comment file incomparable with every hash written after it.
68
+ */
69
+ export declare const ADDRESS_SALT_FILE = "comment-salt";
70
+ /**
71
+ * A submitter's address as the hash a stored record keeps, or `null` when the
72
+ * site could not tell where the request came from.
73
+ *
74
+ * Salted, and the salt is a file under `data/` rather than a constant, because
75
+ * an unsalted hash of an IPv4 address is the address: there are four billion
76
+ * of them and a laptop tries them all in seconds. The comment files are
77
+ * published with the site and go into git, so an address in one would be a
78
+ * reader's home written into a public repository.
79
+ *
80
+ * Losing the salt costs a site the ability to compare old hashes with new
81
+ * ones, and nothing else — which is why it lives beside the keys rather than
82
+ * in the content directory.
83
+ */
84
+ export declare function hashClientAddress(dataDir: string, address: string | undefined): string | null;
85
+ //# sourceMappingURL=protection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protection.d.ts","sourceRoot":"","sources":["../../src/forms/protection.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,YAAY,CAAC;AAEzC,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAE1C,6EAA6E;AAC7E,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,QAAe,CAAC;AAErD,qEAAqE;AACrE,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAQ5E;AAED,qEAAqE;AACrE,MAAM,MAAM,iBAAiB;AAC3B,kEAAkE;AAChE,OAAO;AACT,4CAA4C;GAC1C,WAAW,CAAC;AAEhB;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,iBAAiB,GAAG,SAAS,CAK1F;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAO7F"}
@@ -0,0 +1,116 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { readFileIfPresentSync, writeFileAtomicallySync } from '../files/atomic.js';
4
+ /**
5
+ * What every form on the public site defends itself with.
6
+ *
7
+ * A comment form (TASK-50) and a contact form (TASK-56) are the same problem
8
+ * wearing different fields: an anonymous stranger posting text at a site that
9
+ * has to store it. So the three cheap defences in front of both live here
10
+ * rather than once each, and neither feature can quietly drift into a weaker
11
+ * version of the other's rules.
12
+ *
13
+ * 1. **The honeypot.** A field no person can see and no person fills in. A
14
+ * submission that filled it is dropped without a word, because telling a bot
15
+ * why it failed is telling it how to succeed.
16
+ * 2. **The age of the form.** Something submitted a heartbeat after the page
17
+ * loaded was not typed, and something submitted from a form rendered last
18
+ * year is a replayed body. This is a speed bump rather than a control — the
19
+ * field is not signed, so anything that can read the form can forge it — and
20
+ * it is here because the naive half of the traffic does not bother.
21
+ * 3. **The address hash.** Not a defence in itself but what makes one
22
+ * reviewable: a salted hash of where a submission came from, so a run from
23
+ * one machine is visible without a reader's home address being written down.
24
+ *
25
+ * The fourth, the per-address rate limit, is `createLoginThrottle` in
26
+ * `admin/throttle.ts`, because it is the same limiter the login form uses and
27
+ * it belongs with the rest of that machinery.
28
+ */
29
+ /**
30
+ * The honeypot field, named as something a form-filling robot expects to find
31
+ * and a person never sees.
32
+ *
33
+ * The form hides it from sight and from assistive technology and carries
34
+ * `autocomplete="off"` on it, so a browser does not helpfully fill it in.
35
+ */
36
+ export const FORM_TRAP_FIELD = 'website';
37
+ /** The hidden field carrying when the form was rendered, in epoch milliseconds. */
38
+ export const FORM_LOADED_FIELD = 'loaded';
39
+ /** How long a form has to have been on screen before it may be submitted. */
40
+ export const MINIMUM_SUBMIT_SECONDS = 3;
41
+ /**
42
+ * How long a rendered form stays submittable.
43
+ *
44
+ * A day, so a page left open overnight still works, and a `loaded` from last
45
+ * year — which is what a replayed body looks like — does not.
46
+ */
47
+ export const MAXIMUM_FORM_AGE_SECONDS = 24 * 60 * 60;
48
+ /** Whether the honeypot was filled in, which no person ever does. */
49
+ export function trapped(value) {
50
+ return value.trim() !== '';
51
+ }
52
+ /** How long ago the form was rendered, or `undefined` when it did not say. */
53
+ export function formAgeSeconds(loaded, now) {
54
+ const at = Number(loaded);
55
+ if (!Number.isFinite(at) || at <= 0)
56
+ return undefined;
57
+ const seconds = (now.getTime() - at) / 1000;
58
+ // A form stamped in the future is one somebody wrote by hand, or a clock
59
+ // that moved; either way it is not a form this site rendered a moment ago.
60
+ return seconds < 0 ? undefined : seconds;
61
+ }
62
+ /**
63
+ * Whether the form's age refuses this submission, and why.
64
+ *
65
+ * Staleness is decided before speed, because a `loaded` nothing can read is
66
+ * a forged field rather than a fast typist.
67
+ */
68
+ export function formTimingRefusal(loaded, now) {
69
+ const age = formAgeSeconds(loaded, now);
70
+ if (age === undefined || age > MAXIMUM_FORM_AGE_SECONDS)
71
+ return 'stale';
72
+ if (age < MINIMUM_SUBMIT_SECONDS)
73
+ return 'too-quick';
74
+ return undefined;
75
+ }
76
+ /**
77
+ * Where the salt that hides submitters' addresses lives, under `dataDir`.
78
+ *
79
+ * Named after comments because comments minted it first and a site that has
80
+ * one must go on using it: a new salt would make every hash already in a
81
+ * comment file incomparable with every hash written after it.
82
+ */
83
+ export const ADDRESS_SALT_FILE = 'comment-salt';
84
+ /**
85
+ * A submitter's address as the hash a stored record keeps, or `null` when the
86
+ * site could not tell where the request came from.
87
+ *
88
+ * Salted, and the salt is a file under `data/` rather than a constant, because
89
+ * an unsalted hash of an IPv4 address is the address: there are four billion
90
+ * of them and a laptop tries them all in seconds. The comment files are
91
+ * published with the site and go into git, so an address in one would be a
92
+ * reader's home written into a public repository.
93
+ *
94
+ * Losing the salt costs a site the ability to compare old hashes with new
95
+ * ones, and nothing else — which is why it lives beside the keys rather than
96
+ * in the content directory.
97
+ */
98
+ export function hashClientAddress(dataDir, address) {
99
+ if (address === undefined || address === '')
100
+ return null;
101
+ return createHash('sha256')
102
+ .update(`${addressSalt(dataDir)}:${address}`)
103
+ .digest('hex')
104
+ .slice(0, 32);
105
+ }
106
+ /** The site's address salt, minted on first use. */
107
+ function addressSalt(dataDir) {
108
+ const file = path.join(dataDir, ADDRESS_SALT_FILE);
109
+ const held = readFileIfPresentSync(file)?.trim();
110
+ if (held !== undefined && held !== '')
111
+ return held;
112
+ const minted = randomBytes(32).toString('hex');
113
+ writeFileAtomicallySync(file, `${minted}\n`, { mode: 0o600 });
114
+ return minted;
115
+ }
116
+ //# sourceMappingURL=protection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protection.js","sourceRoot":"","sources":["../../src/forms/protection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,SAAS,CAAC;AAEzC,mFAAmF;AACnF,MAAM,CAAC,MAAM,iBAAiB,GAAG,QAAQ,CAAC;AAE1C,6EAA6E;AAC7E,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAErD,qEAAqE;AACrE,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,GAAS;IACtD,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAEtD,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC;IAC5C,yEAAyE;IACzE,2EAA2E;IAC3E,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC3C,CAAC;AASD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc,EAAE,GAAS;IACzD,MAAM,GAAG,GAAG,cAAc,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACxC,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,GAAG,wBAAwB;QAAE,OAAO,OAAO,CAAC;IACxE,IAAI,GAAG,GAAG,sBAAsB;QAAE,OAAO,WAAW,CAAC;IACrD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,cAAc,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,OAA2B;IAC5E,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAEzD,OAAO,UAAU,CAAC,QAAQ,CAAC;SACxB,MAAM,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,OAAO,EAAE,CAAC;SAC5C,MAAM,CAAC,KAAK,CAAC;SACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC;AAED,oDAAoD;AACpD,SAAS,WAAW,CAAC,OAAe;IAClC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;IACnD,MAAM,IAAI,GAAG,qBAAqB,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;IACjD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAEnD,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC/C,uBAAuB,CAAC,IAAI,EAAE,GAAG,MAAM,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9D,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,59 @@
1
+ import type { ImageConfig } from './paths.ts';
2
+ /**
3
+ * The site's icons: the two favicons and the touch icon, derived from the
4
+ * site's avatar.
5
+ *
6
+ * They are derived copies of an upload like every other file under
7
+ * {@link VARIANT_ASSET_PREFIX} — rebuildable, disposable, never read by a feed
8
+ * or a remote instance — but they are not `srcset` candidates, and they are
9
+ * shaped differently on purpose. A responsive variant is a width and whatever
10
+ * height the picture has; an icon is a square, because that is what
11
+ * `<link rel="icon" sizes="32x32">` promises and what a browser draws in a
12
+ * tab. So an icon is cropped to the middle of the avatar rather than scaled to
13
+ * a width, is always a PNG whatever the site's formats are, and is named
14
+ * `icon-32.png` rather than `32.png` so it can never be confused with the
15
+ * width of that number.
16
+ *
17
+ * Nothing is derived until somebody asks: the head links three URLs, and the
18
+ * first browser to follow one encodes it. That keeps the three sizes out of
19
+ * every upload's variant set, where they would turn up in a `srcset` as
20
+ * candidates nobody wants.
21
+ */
22
+ /** One icon as the head links it. */
23
+ export interface SiteIcon {
24
+ /** `icon` or `apple-touch-icon`. */
25
+ rel: string;
26
+ /** The `sizes` attribute, `32x32`. */
27
+ sizes: string;
28
+ /** Where the derived file is served, as a site-root path. */
29
+ href: string;
30
+ }
31
+ /** The name one size is derived under. */
32
+ export declare function iconFileName(size: number): string;
33
+ /**
34
+ * The size a derived filename names, or `undefined` when it names no icon this
35
+ * site offers.
36
+ *
37
+ * The check the request path goes through, so the URL space cannot be used to
38
+ * make the server encode arbitrary sizes — the rule the widths already follow.
39
+ */
40
+ export declare function iconSize(file: string): number | undefined;
41
+ /**
42
+ * The icons of a site whose avatar is this upload, or none at all.
43
+ *
44
+ * None when the site has no avatar, when the avatar is not one of its own
45
+ * uploads, when it is a file no icon can be made of, or when image
46
+ * optimization is off — because the head must never link a file this server
47
+ * would answer 404 for.
48
+ */
49
+ export declare function siteIcons(config: ImageConfig, avatar: string | undefined): SiteIcon[];
50
+ /**
51
+ * Derive one icon, or answer `false` having written nothing.
52
+ *
53
+ * Enlarging is allowed, unlike a width variant: 180 pixels is what the touch
54
+ * icon has to be, and a site whose avatar is smaller than that is better
55
+ * served a soft icon than none. The crop is the middle of the picture, which
56
+ * is where a face is.
57
+ */
58
+ export declare function deriveSiteIcon(config: ImageConfig, source: string, size: number): Promise<boolean>;
59
+ //# sourceMappingURL=icons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../src/images/icons.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,qCAAqC;AACrC,MAAM,WAAW,QAAQ;IACvB,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;CACd;AAwCD,0CAA0C;AAC1C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGzD;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,EAAE,CAWrF;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,CAAC,CAuBlB"}
@@ -0,0 +1,131 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import sharp from 'sharp';
4
+ import { writeFileAtomically } from '../files/atomic.js';
5
+ import { UPLOAD_ASSET_PREFIX } from '../web/assets.js';
6
+ import { derivedDir, sourceFile, VARIANT_ASSET_PREFIX } from './paths.js';
7
+ /**
8
+ * The icons a site offers, in the order the head lists them: the 32 and 16
9
+ * pixel favicons and the 180 pixel touch icon Apple asks for.
10
+ *
11
+ * Three is the whole set. A site that wants a web manifest, a mask icon or a
12
+ * tile colour writes them into its own theme's `head` block; the package links
13
+ * what every browser reads and nothing that needs a second file to explain it.
14
+ */
15
+ const ICONS = [
16
+ { rel: 'icon', size: 32 },
17
+ { rel: 'icon', size: 16 },
18
+ { rel: 'apple-touch-icon', size: 180 },
19
+ ];
20
+ /** What a derived icon is called inside its source's directory. */
21
+ const ICON_FILE = /^icon-(\d{1,4})\.png$/;
22
+ /**
23
+ * The extensions an icon may be derived from.
24
+ *
25
+ * Wider than the responsive variants' set — an SVG has no pixels to resize but
26
+ * rasterises to a square perfectly well, and only the first frame of a GIF was
27
+ * ever going to be a favicon — and narrower than "anything sharp might open",
28
+ * so a site whose avatar setting points at a PDF links no icons rather than
29
+ * three that 404.
30
+ */
31
+ const ICON_SOURCES = new Set([
32
+ '.avif',
33
+ '.gif',
34
+ '.jpeg',
35
+ '.jpg',
36
+ '.png',
37
+ '.svg',
38
+ '.tif',
39
+ '.tiff',
40
+ '.webp',
41
+ ]);
42
+ /** The name one size is derived under. */
43
+ export function iconFileName(size) {
44
+ return `icon-${String(size)}.png`;
45
+ }
46
+ /**
47
+ * The size a derived filename names, or `undefined` when it names no icon this
48
+ * site offers.
49
+ *
50
+ * The check the request path goes through, so the URL space cannot be used to
51
+ * make the server encode arbitrary sizes — the rule the widths already follow.
52
+ */
53
+ export function iconSize(file) {
54
+ const size = Number(ICON_FILE.exec(file)?.[1] ?? NaN);
55
+ return ICONS.some((icon) => icon.size === size) ? size : undefined;
56
+ }
57
+ /**
58
+ * The icons of a site whose avatar is this upload, or none at all.
59
+ *
60
+ * None when the site has no avatar, when the avatar is not one of its own
61
+ * uploads, when it is a file no icon can be made of, or when image
62
+ * optimization is off — because the head must never link a file this server
63
+ * would answer 404 for.
64
+ */
65
+ export function siteIcons(config, avatar) {
66
+ if (!config.imageOptimization)
67
+ return [];
68
+ const source = iconSource(avatar);
69
+ if (source === undefined)
70
+ return [];
71
+ return ICONS.map(({ rel, size }) => ({
72
+ rel,
73
+ sizes: `${String(size)}x${String(size)}`,
74
+ href: iconHref(source, size),
75
+ }));
76
+ }
77
+ /**
78
+ * Derive one icon, or answer `false` having written nothing.
79
+ *
80
+ * Enlarging is allowed, unlike a width variant: 180 pixels is what the touch
81
+ * icon has to be, and a site whose avatar is smaller than that is better
82
+ * served a soft icon than none. The crop is the middle of the picture, which
83
+ * is where a face is.
84
+ */
85
+ export async function deriveSiteIcon(config, source, size) {
86
+ const file = sourceFile(config, source);
87
+ if (file === undefined || !ICON_SOURCES.has(path.extname(source).toLowerCase()))
88
+ return false;
89
+ const directory = derivedDir(config, source);
90
+ try {
91
+ // `autoOrient` applies the EXIF rotation, and sharp keeps no metadata
92
+ // unless asked, so the icon comes out the right way up carrying no camera
93
+ // and no location.
94
+ const encoded = await sharp(file, { autoOrient: true })
95
+ .resize({ width: size, height: size, fit: 'cover', position: 'centre' })
96
+ .png({ compressionLevel: 9 })
97
+ .toBuffer();
98
+ await mkdir(directory, { recursive: true });
99
+ await writeFileAtomically(path.join(directory, iconFileName(size)), encoded);
100
+ return true;
101
+ }
102
+ catch {
103
+ // An avatar sharp cannot read is a site with no icons, not a failed
104
+ // request: the handler answers 404 and the page it came from is fine.
105
+ return false;
106
+ }
107
+ }
108
+ /** The upload an avatar setting names, or `undefined` when it names none. */
109
+ function iconSource(avatar) {
110
+ if (avatar === undefined || !avatar.startsWith(UPLOAD_ASSET_PREFIX))
111
+ return undefined;
112
+ const relative = avatar.slice(UPLOAD_ASSET_PREFIX.length);
113
+ if (relative === '' || relative.includes('..'))
114
+ return undefined;
115
+ if (!ICON_SOURCES.has(path.extname(relative).toLowerCase()))
116
+ return undefined;
117
+ // The setting holds a URL and the lookup wants a path, so whatever the
118
+ // upload endpoint encoded comes off again here.
119
+ try {
120
+ return relative.split('/').map(decodeURIComponent).join('/');
121
+ }
122
+ catch {
123
+ return undefined;
124
+ }
125
+ }
126
+ /** Where one size of one upload's icon is served. */
127
+ function iconHref(source, size) {
128
+ const segments = [...source.split('/'), iconFileName(size)].map((segment) => encodeURIComponent(segment));
129
+ return `${VARIANT_ASSET_PREFIX}${segments.join('/')}`;
130
+ }
131
+ //# sourceMappingURL=icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.js","sourceRoot":"","sources":["../../src/images/icons.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export { IMAGE_SIZES, responsiveImages, siteImageMarkup } from './markup.ts';
2
+ export type { DescribeImage } from './markup.ts';
3
+ export { siteIcons } from './icons.ts';
4
+ export type { SiteIcon } from './icons.ts';
5
+ export { describeImage, findImageVariant, generateImageVariants, imageMediaType, removeImageVariants, variantUrl, IMAGE_DIRECTORY, IMAGE_RECORD_NAME, VARIANT_ASSET_PREFIX, } from './variants.ts';
6
+ export type { ImageConfig, ImageRecord, ImageVariant } from './variants.ts';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/images/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC7E,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { IMAGE_SIZES, responsiveImages, siteImageMarkup } from './markup.js';
2
+ export { siteIcons } from './icons.js';
3
+ export { describeImage, findImageVariant, generateImageVariants, imageMediaType, removeImageVariants, variantUrl, IMAGE_DIRECTORY, IMAGE_RECORD_NAME, VARIANT_ASSET_PREFIX, } from './variants.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/images/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE7E,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,eAAe,CAAC"}