@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.
- package/README.md +2217 -214
- package/admin/components/fields.njk +165 -0
- package/admin/layouts/base.njk +1 -1
- package/admin/layouts/settings-page.njk +26 -0
- package/admin/layouts/shell.njk +17 -3
- package/admin/pages/account/forgot.njk +47 -0
- package/admin/pages/account/login.njk +26 -0
- package/admin/pages/account/reset.njk +38 -0
- package/admin/pages/account/setup.njk +28 -0
- package/admin/pages/appearance/themes.njk +69 -0
- package/admin/pages/comments/all.njk +133 -0
- package/admin/{layouts/dashboard.njk → pages/dashboard/home.njk} +8 -15
- package/admin/{layouts/document-conflict.njk → pages/documents/conflict.njk} +1 -0
- package/admin/{layouts/document-editor.njk → pages/documents/editor.njk} +75 -59
- package/admin/{layouts/document-list.njk → pages/documents/list.njk} +19 -2
- package/admin/pages/documents/taxonomy.njk +95 -0
- package/admin/pages/federation/followers.njk +238 -0
- package/admin/pages/federation/settings.njk +77 -0
- package/admin/pages/media/library.njk +160 -0
- package/admin/pages/messages/all.njk +110 -0
- package/admin/pages/navigation/menus.njk +129 -0
- package/admin/{layouts → pages}/placeholder.njk +5 -1
- package/admin/pages/settings/discussion.njk +99 -0
- package/admin/pages/settings/email.njk +203 -0
- package/admin/pages/settings/general.njk +29 -0
- package/admin/pages/settings/permalinks.njk +50 -0
- package/admin/pages/settings/reading.njk +64 -0
- package/admin/pages/tools/content-index.njk +89 -0
- package/admin/pages/users/edit.njk +239 -0
- package/admin/pages/users/list.njk +51 -0
- package/admin/pages/users/new.njk +41 -0
- package/admin/static/admin.css +558 -3
- package/admin/static/copy.js +52 -0
- package/admin/static/editor.js +16 -16
- package/admin/static/slug.js +61 -0
- package/dist/access-log.d.ts +44 -0
- package/dist/access-log.d.ts.map +1 -0
- package/dist/access-log.js +71 -0
- package/dist/access-log.js.map +1 -0
- package/dist/admin/accounts.d.ts +413 -0
- package/dist/admin/accounts.d.ts.map +1 -0
- package/dist/admin/accounts.js +680 -0
- package/dist/admin/accounts.js.map +1 -0
- package/dist/admin/appearance.d.ts +80 -0
- package/dist/admin/appearance.d.ts.map +1 -0
- package/dist/admin/appearance.js +136 -0
- package/dist/admin/appearance.js.map +1 -0
- package/dist/admin/comments.d.ts +90 -0
- package/dist/admin/comments.d.ts.map +1 -0
- package/dist/admin/comments.js +244 -0
- package/dist/admin/comments.js.map +1 -0
- package/dist/admin/credentials.d.ts +26 -2
- package/dist/admin/credentials.d.ts.map +1 -1
- package/dist/admin/credentials.js +35 -2
- package/dist/admin/credentials.js.map +1 -1
- package/dist/admin/documents.d.ts +117 -10
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +328 -41
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/federation.d.ts +161 -50
- package/dist/admin/federation.d.ts.map +1 -1
- package/dist/admin/federation.js +235 -93
- package/dist/admin/federation.js.map +1 -1
- package/dist/admin/formatting.d.ts +20 -0
- package/dist/admin/formatting.d.ts.map +1 -0
- package/dist/admin/formatting.js +72 -0
- package/dist/admin/formatting.js.map +1 -0
- package/dist/admin/headers.d.ts +65 -0
- package/dist/admin/headers.d.ts.map +1 -0
- package/dist/admin/headers.js +104 -0
- package/dist/admin/headers.js.map +1 -0
- package/dist/admin/index.d.ts +38 -12
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +25 -7
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/media.d.ts +168 -0
- package/dist/admin/media.d.ts.map +1 -0
- package/dist/admin/media.js +295 -0
- package/dist/admin/media.js.map +1 -0
- package/dist/admin/menu.d.ts +79 -0
- package/dist/admin/menu.d.ts.map +1 -0
- package/dist/admin/menu.js +143 -0
- package/dist/admin/menu.js.map +1 -0
- package/dist/admin/messages.d.ts +80 -0
- package/dist/admin/messages.d.ts.map +1 -0
- package/dist/admin/messages.js +152 -0
- package/dist/admin/messages.js.map +1 -0
- package/dist/admin/navigation.d.ts +155 -0
- package/dist/admin/navigation.d.ts.map +1 -0
- package/dist/admin/navigation.js +292 -0
- package/dist/admin/navigation.js.map +1 -0
- package/dist/admin/preview.d.ts.map +1 -1
- package/dist/admin/preview.js +31 -7
- package/dist/admin/preview.js.map +1 -1
- package/dist/admin/recovery.d.ts +92 -0
- package/dist/admin/recovery.d.ts.map +1 -0
- package/dist/admin/recovery.js +229 -0
- package/dist/admin/recovery.js.map +1 -0
- package/dist/admin/routes.d.ts +0 -20
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +164 -105
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/session.d.ts +17 -3
- package/dist/admin/session.d.ts.map +1 -1
- package/dist/admin/session.js +19 -5
- package/dist/admin/session.js.map +1 -1
- package/dist/admin/settings-discussion.d.ts +37 -0
- package/dist/admin/settings-discussion.d.ts.map +1 -0
- package/dist/admin/settings-discussion.js +117 -0
- package/dist/admin/settings-discussion.js.map +1 -0
- package/dist/admin/settings-email.d.ts +54 -0
- package/dist/admin/settings-email.d.ts.map +1 -0
- package/dist/admin/settings-email.js +189 -0
- package/dist/admin/settings-email.js.map +1 -0
- package/dist/admin/settings-federation.d.ts +48 -0
- package/dist/admin/settings-federation.d.ts.map +1 -0
- package/dist/admin/settings-federation.js +123 -0
- package/dist/admin/settings-federation.js.map +1 -0
- package/dist/admin/settings-general.d.ts +12 -0
- package/dist/admin/settings-general.d.ts.map +1 -0
- package/dist/admin/settings-general.js +48 -0
- package/dist/admin/settings-general.js.map +1 -0
- package/dist/admin/settings-page.d.ts +119 -0
- package/dist/admin/settings-page.d.ts.map +1 -0
- package/dist/admin/settings-page.js +132 -0
- package/dist/admin/settings-page.js.map +1 -0
- package/dist/admin/settings-pages.d.ts +30 -0
- package/dist/admin/settings-pages.d.ts.map +1 -0
- package/dist/admin/settings-pages.js +43 -0
- package/dist/admin/settings-pages.js.map +1 -0
- package/dist/admin/settings-permalinks.d.ts +16 -0
- package/dist/admin/settings-permalinks.d.ts.map +1 -0
- package/dist/admin/settings-permalinks.js +32 -0
- package/dist/admin/settings-permalinks.js.map +1 -0
- package/dist/admin/settings-reading.d.ts +34 -0
- package/dist/admin/settings-reading.d.ts.map +1 -0
- package/dist/admin/settings-reading.js +49 -0
- package/dist/admin/settings-reading.js.map +1 -0
- package/dist/admin/settings.d.ts +339 -96
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +600 -308
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/store.d.ts +606 -152
- package/dist/admin/store.d.ts.map +1 -1
- package/dist/admin/store.js +863 -268
- package/dist/admin/store.js.map +1 -1
- package/dist/admin/taxonomy.d.ts +114 -0
- package/dist/admin/taxonomy.d.ts.map +1 -0
- package/dist/admin/taxonomy.js +289 -0
- package/dist/admin/taxonomy.js.map +1 -0
- package/dist/admin/templates.d.ts +46 -14
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +46 -14
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/throttle.d.ts +84 -0
- package/dist/admin/throttle.d.ts.map +1 -0
- package/dist/admin/throttle.js +143 -0
- package/dist/admin/throttle.js.map +1 -0
- package/dist/admin/tools.d.ts +111 -0
- package/dist/admin/tools.d.ts.map +1 -0
- package/dist/admin/tools.js +182 -0
- package/dist/admin/tools.js.map +1 -0
- package/dist/admin/uploads.d.ts +28 -2
- package/dist/admin/uploads.d.ts.map +1 -1
- package/dist/admin/uploads.js +43 -4
- package/dist/admin/uploads.js.map +1 -1
- package/dist/admin/users.d.ts +100 -13
- package/dist/admin/users.d.ts.map +1 -1
- package/dist/admin/users.js +412 -39
- package/dist/admin/users.js.map +1 -1
- package/dist/cache.d.ts +141 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +207 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +14 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +429 -21
- package/dist/cli.js.map +1 -1
- package/dist/comments/akismet.d.ts +130 -0
- package/dist/comments/akismet.d.ts.map +1 -0
- package/dist/comments/akismet.js +274 -0
- package/dist/comments/akismet.js.map +1 -0
- package/dist/comments/conversation.d.ts +29 -0
- package/dist/comments/conversation.d.ts.map +1 -0
- package/dist/comments/conversation.js +60 -0
- package/dist/comments/conversation.js.map +1 -0
- package/dist/comments/form.d.ts +123 -0
- package/dist/comments/form.d.ts.map +1 -0
- package/dist/comments/form.js +79 -0
- package/dist/comments/form.js.map +1 -0
- package/dist/comments/index.d.ts +16 -0
- package/dist/comments/index.d.ts.map +1 -0
- package/dist/comments/index.js +10 -0
- package/dist/comments/index.js.map +1 -0
- package/dist/comments/markdown.d.ts +8 -0
- package/dist/comments/markdown.d.ts.map +1 -0
- package/dist/comments/markdown.js +85 -0
- package/dist/comments/markdown.js.map +1 -0
- package/dist/comments/moderate.d.ts +71 -0
- package/dist/comments/moderate.d.ts.map +1 -0
- package/dist/comments/moderate.js +88 -0
- package/dist/comments/moderate.js.map +1 -0
- package/dist/comments/policy.d.ts +57 -0
- package/dist/comments/policy.d.ts.map +1 -0
- package/dist/comments/policy.js +80 -0
- package/dist/comments/policy.js.map +1 -0
- package/dist/comments/records.d.ts +266 -0
- package/dist/comments/records.d.ts.map +1 -0
- package/dist/comments/records.js +487 -0
- package/dist/comments/records.js.map +1 -0
- package/dist/comments/routes.d.ts +60 -0
- package/dist/comments/routes.d.ts.map +1 -0
- package/dist/comments/routes.js +212 -0
- package/dist/comments/routes.js.map +1 -0
- package/dist/comments/submission.d.ts +297 -0
- package/dist/comments/submission.d.ts.map +1 -0
- package/dist/comments/submission.js +280 -0
- package/dist/comments/submission.js.map +1 -0
- package/dist/comments/viewer.d.ts +21 -0
- package/dist/comments/viewer.d.ts.map +1 -0
- package/dist/comments/viewer.js +45 -0
- package/dist/comments/viewer.js.map +1 -0
- package/dist/config.d.ts +225 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +121 -6
- package/dist/config.js.map +1 -1
- package/dist/contact/delivery.d.ts +60 -0
- package/dist/contact/delivery.d.ts.map +1 -0
- package/dist/contact/delivery.js +87 -0
- package/dist/contact/delivery.js.map +1 -0
- package/dist/contact/form.d.ts +130 -0
- package/dist/contact/form.d.ts.map +1 -0
- package/dist/contact/form.js +163 -0
- package/dist/contact/form.js.map +1 -0
- package/dist/contact/index.d.ts +17 -0
- package/dist/contact/index.d.ts.map +1 -0
- package/dist/contact/index.js +13 -0
- package/dist/contact/index.js.map +1 -0
- package/dist/contact/records.d.ts +124 -0
- package/dist/contact/records.d.ts.map +1 -0
- package/dist/contact/records.js +218 -0
- package/dist/contact/records.js.map +1 -0
- package/dist/contact/routes.d.ts +17 -0
- package/dist/contact/routes.d.ts.map +1 -0
- package/dist/contact/routes.js +127 -0
- package/dist/contact/routes.js.map +1 -0
- package/dist/contact/submission.d.ts +103 -0
- package/dist/contact/submission.d.ts.map +1 -0
- package/dist/contact/submission.js +166 -0
- package/dist/contact/submission.js.map +1 -0
- package/dist/content/document.d.ts +7 -2
- package/dist/content/document.d.ts.map +1 -1
- package/dist/content/document.js +1 -0
- package/dist/content/document.js.map +1 -1
- package/dist/content/index.d.ts +7 -2
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -1
- package/dist/content/index.js.map +1 -1
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +24 -2
- package/dist/content/markdown.js.map +1 -1
- package/dist/content/parser.d.ts.map +1 -1
- package/dist/content/parser.js +11 -4
- package/dist/content/parser.js.map +1 -1
- package/dist/content/save.d.ts +5 -0
- package/dist/content/save.d.ts.map +1 -1
- package/dist/content/save.js +44 -1
- package/dist/content/save.js.map +1 -1
- package/dist/content/schedule.d.ts +125 -0
- package/dist/content/schedule.d.ts.map +1 -0
- package/dist/content/schedule.js +207 -0
- package/dist/content/schedule.js.map +1 -0
- package/dist/content/search.d.ts +64 -0
- package/dist/content/search.d.ts.map +1 -0
- package/dist/content/search.js +136 -0
- package/dist/content/search.js.map +1 -0
- package/dist/content/store.d.ts +220 -7
- package/dist/content/store.d.ts.map +1 -1
- package/dist/content/store.js +392 -67
- package/dist/content/store.js.map +1 -1
- package/dist/content/sync.d.ts +7 -1
- package/dist/content/sync.d.ts.map +1 -1
- package/dist/content/sync.js +18 -10
- package/dist/content/sync.js.map +1 -1
- package/dist/content/time.d.ts +56 -0
- package/dist/content/time.d.ts.map +1 -0
- package/dist/content/time.js +141 -0
- package/dist/content/time.js.map +1 -0
- package/dist/content/writer.d.ts.map +1 -1
- package/dist/content/writer.js +3 -0
- package/dist/content/writer.js.map +1 -1
- package/dist/env.d.ts +70 -5
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/actor.d.ts +95 -41
- package/dist/federation/actor.d.ts.map +1 -1
- package/dist/federation/actor.js +180 -53
- package/dist/federation/actor.js.map +1 -1
- package/dist/federation/article.d.ts +44 -17
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +98 -46
- package/dist/federation/article.js.map +1 -1
- package/dist/federation/delivery.d.ts +63 -15
- package/dist/federation/delivery.d.ts.map +1 -1
- package/dist/federation/delivery.js +154 -80
- package/dist/federation/delivery.js.map +1 -1
- package/dist/federation/federation.d.ts +22 -17
- package/dist/federation/federation.d.ts.map +1 -1
- package/dist/federation/federation.js +98 -77
- package/dist/federation/federation.js.map +1 -1
- package/dist/federation/followers.d.ts +3 -2
- package/dist/federation/followers.d.ts.map +1 -1
- package/dist/federation/followers.js +6 -5
- package/dist/federation/followers.js.map +1 -1
- package/dist/federation/import-wordpress.d.ts +128 -0
- package/dist/federation/import-wordpress.d.ts.map +1 -0
- package/dist/federation/import-wordpress.js +426 -0
- package/dist/federation/import-wordpress.js.map +1 -0
- package/dist/federation/inbox.d.ts +36 -13
- package/dist/federation/inbox.d.ts.map +1 -1
- package/dist/federation/inbox.js +120 -49
- package/dist/federation/inbox.js.map +1 -1
- package/dist/federation/index.d.ts +22 -10
- package/dist/federation/index.d.ts.map +1 -1
- package/dist/federation/index.js +13 -8
- package/dist/federation/index.js.map +1 -1
- package/dist/federation/keys.d.ts +98 -20
- package/dist/federation/keys.d.ts.map +1 -1
- package/dist/federation/keys.js +269 -39
- package/dist/federation/keys.js.map +1 -1
- package/dist/federation/mount.d.ts +46 -10
- package/dist/federation/mount.d.ts.map +1 -1
- package/dist/federation/mount.js +300 -28
- package/dist/federation/mount.js.map +1 -1
- package/dist/federation/paths.d.ts +23 -35
- package/dist/federation/paths.d.ts.map +1 -1
- package/dist/federation/paths.js +26 -39
- package/dist/federation/paths.js.map +1 -1
- package/dist/federation/records.d.ts +223 -0
- package/dist/federation/records.d.ts.map +1 -0
- package/dist/federation/records.js +506 -0
- package/dist/federation/records.js.map +1 -0
- package/dist/federation/relays.d.ts +114 -0
- package/dist/federation/relays.d.ts.map +1 -0
- package/dist/federation/relays.js +290 -0
- package/dist/federation/relays.js.map +1 -0
- package/dist/federation/replies.d.ts +80 -0
- package/dist/federation/replies.d.ts.map +1 -0
- package/dist/federation/replies.js +166 -0
- package/dist/federation/replies.js.map +1 -0
- package/dist/federation/wordpress.d.ts +131 -0
- package/dist/federation/wordpress.d.ts.map +1 -0
- package/dist/federation/wordpress.js +341 -0
- package/dist/federation/wordpress.js.map +1 -0
- package/dist/files/atomic.d.ts +103 -0
- package/dist/files/atomic.d.ts.map +1 -0
- package/dist/files/atomic.js +150 -0
- package/dist/files/atomic.js.map +1 -0
- package/dist/files/index.d.ts +3 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +2 -0
- package/dist/files/index.js.map +1 -0
- package/dist/forms/protection.d.ts +85 -0
- package/dist/forms/protection.d.ts.map +1 -0
- package/dist/forms/protection.js +116 -0
- package/dist/forms/protection.js.map +1 -0
- package/dist/images/icons.d.ts +59 -0
- package/dist/images/icons.d.ts.map +1 -0
- package/dist/images/icons.js +131 -0
- package/dist/images/icons.js.map +1 -0
- package/dist/images/index.d.ts +7 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +4 -0
- package/dist/images/index.js.map +1 -0
- package/dist/images/markup.d.ts +47 -0
- package/dist/images/markup.d.ts.map +1 -0
- package/dist/images/markup.js +175 -0
- package/dist/images/markup.js.map +1 -0
- package/dist/images/paths.d.ts +34 -0
- package/dist/images/paths.d.ts.map +1 -0
- package/dist/images/paths.js +41 -0
- package/dist/images/paths.js.map +1 -0
- package/dist/images/variants.d.ts +104 -0
- package/dist/images/variants.d.ts.map +1 -0
- package/dist/images/variants.js +320 -0
- package/dist/images/variants.js.map +1 -0
- package/dist/index.d.ts +123 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +344 -25
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +18 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +34 -3
- package/dist/init.js.map +1 -1
- package/dist/mail/brevo.d.ts +35 -0
- package/dist/mail/brevo.d.ts.map +1 -0
- package/dist/mail/brevo.js +85 -0
- package/dist/mail/brevo.js.map +1 -0
- package/dist/mail/credentials.d.ts +62 -0
- package/dist/mail/credentials.d.ts.map +1 -0
- package/dist/mail/credentials.js +86 -0
- package/dist/mail/credentials.js.map +1 -0
- package/dist/mail/index.d.ts +15 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +8 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/mail/memory.d.ts +35 -0
- package/dist/mail/memory.d.ts.map +1 -0
- package/dist/mail/memory.js +29 -0
- package/dist/mail/memory.js.map +1 -0
- package/dist/mail/provider.d.ts +80 -0
- package/dist/mail/provider.d.ts.map +1 -0
- package/dist/mail/provider.js +23 -0
- package/dist/mail/provider.js.map +1 -0
- package/dist/mail/service.d.ts +156 -0
- package/dist/mail/service.d.ts.map +1 -0
- package/dist/mail/service.js +265 -0
- package/dist/mail/service.js.map +1 -0
- package/dist/mail/smtp.d.ts +39 -0
- package/dist/mail/smtp.d.ts.map +1 -0
- package/dist/mail/smtp.js +58 -0
- package/dist/mail/smtp.js.map +1 -0
- package/dist/mail/templates.d.ts +91 -0
- package/dist/mail/templates.d.ts.map +1 -0
- package/dist/mail/templates.js +69 -0
- package/dist/mail/templates.js.map +1 -0
- package/dist/notifications/comments.d.ts +66 -0
- package/dist/notifications/comments.d.ts.map +1 -0
- package/dist/notifications/comments.js +151 -0
- package/dist/notifications/comments.js.map +1 -0
- package/dist/notifications/digest.d.ts +135 -0
- package/dist/notifications/digest.d.ts.map +1 -0
- package/dist/notifications/digest.js +287 -0
- package/dist/notifications/digest.js.map +1 -0
- package/dist/notifications/index.d.ts +13 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +8 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/links.d.ts +60 -0
- package/dist/notifications/links.d.ts.map +1 -0
- package/dist/notifications/links.js +63 -0
- package/dist/notifications/links.js.map +1 -0
- package/dist/notifications/optouts.d.ts +48 -0
- package/dist/notifications/optouts.d.ts.map +1 -0
- package/dist/notifications/optouts.js +93 -0
- package/dist/notifications/optouts.js.map +1 -0
- package/dist/notifications/preferences.d.ts +158 -0
- package/dist/notifications/preferences.d.ts.map +1 -0
- package/dist/notifications/preferences.js +195 -0
- package/dist/notifications/preferences.js.map +1 -0
- package/dist/notifications/routes.d.ts +28 -0
- package/dist/notifications/routes.d.ts.map +1 -0
- package/dist/notifications/routes.js +184 -0
- package/dist/notifications/routes.js.map +1 -0
- package/dist/notifications/tokens.d.ts +60 -0
- package/dist/notifications/tokens.d.ts.map +1 -0
- package/dist/notifications/tokens.js +125 -0
- package/dist/notifications/tokens.js.map +1 -0
- package/dist/notify.d.ts +84 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +149 -0
- package/dist/notify.js.map +1 -0
- package/dist/web/archive.d.ts +49 -0
- package/dist/web/archive.d.ts.map +1 -0
- package/dist/web/archive.js +59 -0
- package/dist/web/archive.js.map +1 -0
- package/dist/web/assets.d.ts +6 -5
- package/dist/web/assets.d.ts.map +1 -1
- package/dist/web/assets.js +5 -7
- package/dist/web/assets.js.map +1 -1
- package/dist/web/authors.d.ts +143 -0
- package/dist/web/authors.d.ts.map +1 -0
- package/dist/web/authors.js +178 -0
- package/dist/web/authors.js.map +1 -0
- package/dist/web/comments.d.ts +60 -0
- package/dist/web/comments.d.ts.map +1 -0
- package/dist/web/comments.js +207 -0
- package/dist/web/comments.js.map +1 -0
- package/dist/web/context.d.ts +162 -28
- package/dist/web/context.d.ts.map +1 -1
- package/dist/web/context.js +123 -26
- package/dist/web/context.js.map +1 -1
- package/dist/web/conversation.d.ts +209 -0
- package/dist/web/conversation.d.ts.map +1 -0
- package/dist/web/conversation.js +557 -0
- package/dist/web/conversation.js.map +1 -0
- package/dist/web/documents.d.ts +36 -6
- package/dist/web/documents.d.ts.map +1 -1
- package/dist/web/documents.js +52 -10
- package/dist/web/documents.js.map +1 -1
- package/dist/web/feed-atom.d.ts +22 -0
- package/dist/web/feed-atom.d.ts.map +1 -0
- package/dist/web/feed-atom.js +83 -0
- package/dist/web/feed-atom.js.map +1 -0
- package/dist/web/feed-item.d.ts +145 -0
- package/dist/web/feed-item.d.ts.map +1 -0
- package/dist/web/feed-item.js +120 -0
- package/dist/web/feed-item.js.map +1 -0
- package/dist/web/feed-json.d.ts +79 -0
- package/dist/web/feed-json.d.ts.map +1 -0
- package/dist/web/feed-json.js +53 -0
- package/dist/web/feed-json.js.map +1 -0
- package/dist/web/feed-rss.d.ts +26 -0
- package/dist/web/feed-rss.d.ts.map +1 -0
- package/dist/web/feed-rss.js +203 -0
- package/dist/web/feed-rss.js.map +1 -0
- package/dist/web/feed-source.d.ts +244 -0
- package/dist/web/feed-source.d.ts.map +1 -0
- package/dist/web/feed-source.js +201 -0
- package/dist/web/feed-source.js.map +1 -0
- package/dist/web/feed-xml.d.ts +63 -0
- package/dist/web/feed-xml.d.ts.map +1 -0
- package/dist/web/feed-xml.js +116 -0
- package/dist/web/feed-xml.js.map +1 -0
- package/dist/web/feeds.d.ts +31 -96
- package/dist/web/feeds.d.ts.map +1 -1
- package/dist/web/feeds.js +94 -198
- package/dist/web/feeds.js.map +1 -1
- package/dist/web/health.d.ts +31 -0
- package/dist/web/health.d.ts.map +1 -0
- package/dist/web/health.js +44 -0
- package/dist/web/health.js.map +1 -0
- package/dist/web/index.d.ts +30 -10
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +18 -7
- package/dist/web/index.js.map +1 -1
- package/dist/web/navigation.d.ts +195 -0
- package/dist/web/navigation.d.ts.map +1 -0
- package/dist/web/navigation.js +300 -0
- package/dist/web/navigation.js.map +1 -0
- package/dist/web/negotiate.d.ts +31 -0
- package/dist/web/negotiate.d.ts.map +1 -1
- package/dist/web/negotiate.js +31 -11
- package/dist/web/negotiate.js.map +1 -1
- package/dist/web/recent.d.ts +43 -0
- package/dist/web/recent.d.ts.map +1 -0
- package/dist/web/recent.js +37 -0
- package/dist/web/recent.js.map +1 -0
- package/dist/web/render.d.ts +203 -9
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +341 -17
- package/dist/web/render.js.map +1 -1
- package/dist/web/routes.d.ts +25 -8
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +854 -93
- package/dist/web/routes.js.map +1 -1
- package/dist/web/sanitize.d.ts +19 -0
- package/dist/web/sanitize.d.ts.map +1 -0
- package/dist/web/sanitize.js +272 -0
- package/dist/web/sanitize.js.map +1 -0
- package/dist/web/search.d.ts +79 -0
- package/dist/web/search.d.ts.map +1 -0
- package/dist/web/search.js +94 -0
- package/dist/web/search.js.map +1 -0
- package/dist/web/sitemap.d.ts +120 -0
- package/dist/web/sitemap.d.ts.map +1 -0
- package/dist/web/sitemap.js +199 -0
- package/dist/web/sitemap.js.map +1 -0
- package/dist/web/taxonomy.d.ts +152 -0
- package/dist/web/taxonomy.d.ts.map +1 -0
- package/dist/web/taxonomy.js +237 -0
- package/dist/web/taxonomy.js.map +1 -0
- package/dist/web/templates.d.ts +54 -16
- package/dist/web/templates.d.ts.map +1 -1
- package/dist/web/templates.js +109 -28
- package/dist/web/templates.js.map +1 -1
- package/dist/web/themes.d.ts +226 -0
- package/dist/web/themes.d.ts.map +1 -0
- package/dist/web/themes.js +314 -0
- package/dist/web/themes.js.map +1 -0
- package/dist/webmention/discovery.d.ts +51 -0
- package/dist/webmention/discovery.d.ts.map +1 -0
- package/dist/webmention/discovery.js +176 -0
- package/dist/webmention/discovery.js.map +1 -0
- package/dist/webmention/html.d.ts +71 -0
- package/dist/webmention/html.d.ts.map +1 -0
- package/dist/webmention/html.js +285 -0
- package/dist/webmention/html.js.map +1 -0
- package/dist/webmention/index.d.ts +12 -0
- package/dist/webmention/index.d.ts.map +1 -0
- package/dist/webmention/index.js +8 -0
- package/dist/webmention/index.js.map +1 -0
- package/dist/webmention/links.d.ts +18 -0
- package/dist/webmention/links.d.ts.map +1 -0
- package/dist/webmention/links.js +64 -0
- package/dist/webmention/links.js.map +1 -0
- package/dist/webmention/microformats.d.ts +93 -0
- package/dist/webmention/microformats.d.ts.map +1 -0
- package/dist/webmention/microformats.js +376 -0
- package/dist/webmention/microformats.js.map +1 -0
- package/dist/webmention/receive.d.ts +141 -0
- package/dist/webmention/receive.d.ts.map +1 -0
- package/dist/webmention/receive.js +264 -0
- package/dist/webmention/receive.js.map +1 -0
- package/dist/webmention/routes.d.ts +37 -0
- package/dist/webmention/routes.d.ts.map +1 -0
- package/dist/webmention/routes.js +75 -0
- package/dist/webmention/routes.js.map +1 -0
- package/dist/webmention/service.d.ts +94 -0
- package/dist/webmention/service.d.ts.map +1 -0
- package/dist/webmention/service.js +207 -0
- package/dist/webmention/service.js.map +1 -0
- package/package.json +11 -6
- package/templates/site/content/_data/site.json +8 -0
- package/templates/site/content/pages/about.md +7 -0
- package/templates/site/content/posts/2026-01-01-hello-world.md +5 -3
- package/templates/site/geekity.config.ts +11 -1
- package/templates/site/tsconfig.json +1 -1
- package/themes/default/README.md +1216 -50
- package/themes/default/layouts/404.njk +2 -3
- package/themes/default/layouts/author.njk +36 -0
- package/themes/default/layouts/base.njk +161 -31
- package/themes/default/layouts/category.njk +27 -0
- package/themes/default/layouts/front-page.njk +56 -0
- package/themes/default/layouts/home.njk +17 -1
- package/themes/default/layouts/page.njk +57 -5
- package/themes/default/layouts/post.njk +78 -19
- package/themes/default/layouts/search.njk +57 -0
- package/themes/default/layouts/tag.njk +3 -6
- package/themes/default/mail/comment-digest.html.njk +37 -0
- package/themes/default/mail/comment-digest.subject.njk +3 -0
- package/themes/default/mail/comment-digest.txt.njk +40 -0
- package/themes/default/mail/comment-pending.html.njk +27 -0
- package/themes/default/mail/comment-pending.subject.njk +2 -0
- package/themes/default/mail/comment-pending.txt.njk +24 -0
- package/themes/default/mail/comment-reply.html.njk +21 -0
- package/themes/default/mail/comment-reply.subject.njk +2 -0
- package/themes/default/mail/comment-reply.txt.njk +22 -0
- package/themes/default/mail/contact-message.html.njk +24 -0
- package/themes/default/mail/contact-message.subject.njk +5 -0
- package/themes/default/mail/contact-message.txt.njk +20 -0
- package/themes/default/mail/password-changed.html.njk +18 -0
- package/themes/default/mail/password-changed.subject.njk +2 -0
- package/themes/default/mail/password-changed.txt.njk +17 -0
- package/themes/default/mail/password-reset.html.njk +22 -0
- package/themes/default/mail/password-reset.subject.njk +2 -0
- package/themes/default/mail/password-reset.txt.njk +18 -0
- package/themes/default/mail/test.html.njk +14 -0
- package/themes/default/mail/test.subject.njk +2 -0
- package/themes/default/mail/test.txt.njk +12 -0
- package/themes/default/partials/archive.njk +27 -0
- package/themes/default/partials/bio.njk +71 -0
- package/themes/default/partials/comment-form.njk +137 -0
- package/themes/default/partials/contact-form.njk +88 -0
- package/themes/default/partials/conversation.njk +139 -0
- package/themes/default/partials/feeds.njk +26 -0
- package/themes/default/partials/jsonld.njk +120 -0
- package/themes/default/partials/menu.njk +32 -0
- package/themes/default/partials/pagination.njk +12 -6
- package/themes/default/partials/post-list.njk +51 -18
- package/themes/default/partials/search-form.njk +22 -0
- package/themes/default/partials/tags.njk +26 -5
- package/themes/default/static/highlight.js +4 -0
- package/themes/default/static/style.css +1142 -194
- package/themes/default/theme.json +9 -0
- package/admin/layouts/federation.njk +0 -145
- package/admin/layouts/login.njk +0 -23
- package/admin/layouts/settings.njk +0 -116
- package/admin/layouts/setup.njk +0 -27
- package/admin/layouts/users.njk +0 -91
- /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 @@
|
|
|
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 @@
|
|
|
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"}
|