@geekity/cms 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2091 -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/pages/documents/editor.njk +196 -0
- 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/media/library.njk +160 -0
- package/admin/pages/messages/all.njk +110 -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/federation.njk +75 -0
- package/admin/pages/settings/general.njk +29 -0
- package/admin/pages/settings/permalinks.njk +50 -0
- package/admin/pages/settings/reading.njk +72 -0
- package/admin/pages/users/edit.njk +239 -0
- package/admin/pages/users/list.njk +48 -0
- package/admin/pages/users/new.njk +41 -0
- package/admin/static/admin.css +498 -3
- package/admin/static/copy.js +52 -0
- package/admin/static/editor.js +16 -16
- package/admin/static/slug.js +61 -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 +127 -10
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +360 -41
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/federation.d.ts +151 -50
- package/dist/admin/federation.d.ts.map +1 -1
- package/dist/admin/federation.js +225 -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 +126 -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/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 +155 -105
- package/dist/admin/routes.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 +39 -0
- package/dist/admin/settings-federation.d.ts.map +1 -0
- package/dist/admin/settings-federation.js +103 -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 +102 -0
- package/dist/admin/settings-page.d.ts.map +1 -0
- package/dist/admin/settings-page.js +126 -0
- package/dist/admin/settings-page.js.map +1 -0
- package/dist/admin/settings-pages.d.ts +19 -0
- package/dist/admin/settings-pages.d.ts.map +1 -0
- package/dist/admin/settings-pages.js +33 -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 +33 -0
- package/dist/admin/settings-reading.d.ts.map +1 -0
- package/dist/admin/settings-reading.js +48 -0
- package/dist/admin/settings-reading.js.map +1 -0
- package/dist/admin/settings.d.ts +325 -96
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +650 -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 +44 -14
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +44 -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/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 +424 -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 +75 -0
- package/dist/comments/form.d.ts.map +1 -0
- package/dist/comments/form.js +64 -0
- package/dist/comments/form.js.map +1 -0
- package/dist/comments/index.d.ts +15 -0
- package/dist/comments/index.d.ts.map +1 -0
- package/dist/comments/index.js +9 -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 +58 -0
- package/dist/comments/routes.d.ts.map +1 -0
- package/dist/comments/routes.js +177 -0
- package/dist/comments/routes.js.map +1 -0
- package/dist/comments/submission.d.ts +263 -0
- package/dist/comments/submission.d.ts.map +1 -0
- package/dist/comments/submission.js +241 -0
- package/dist/comments/submission.js.map +1 -0
- package/dist/config.d.ts +190 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +118 -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 +202 -7
- package/dist/content/store.d.ts.map +1 -1
- package/dist/content/store.js +374 -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 +58 -4
- 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 +42 -10
- package/dist/federation/mount.d.ts.map +1 -1
- package/dist/federation/mount.js +266 -26
- 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 +327 -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 +73 -0
- package/dist/web/navigation.d.ts.map +1 -0
- package/dist/web/navigation.js +110 -0
- package/dist/web/navigation.js.map +1 -0
- package/dist/web/negotiate.d.ts +9 -0
- package/dist/web/negotiate.d.ts.map +1 -1
- package/dist/web/negotiate.js +6 -1
- 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 +197 -9
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +334 -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 +845 -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 +205 -0
- package/dist/web/themes.d.ts.map +1 -0
- package/dist/web/themes.js +277 -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 +1 -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 +1096 -50
- package/themes/default/layouts/404.njk +2 -3
- package/themes/default/layouts/author.njk +37 -0
- package/themes/default/layouts/base.njk +172 -31
- package/themes/default/layouts/category.njk +27 -0
- package/themes/default/layouts/front-page.njk +57 -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 +77 -0
- package/themes/default/partials/comment-form.njk +109 -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/pagination.njk +12 -6
- package/themes/default/partials/post-list.njk +51 -18
- package/themes/default/partials/search-form.njk +17 -0
- package/themes/default/partials/tags.njk +26 -5
- package/themes/default/static/highlight.js +4 -0
- package/themes/default/static/style.css +1119 -194
- package/themes/default/theme.json +5 -0
- package/admin/layouts/document-editor.njk +0 -167
- 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,158 @@
|
|
|
1
|
+
import type { User } from '../admin/accounts.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Which notices a user wants, as a switchboard rather than a field per notice.
|
|
4
|
+
*
|
|
5
|
+
* The point of this module is that adding the next event — a new follower, a
|
|
6
|
+
* digest of what could not be delivered — is one entry in
|
|
7
|
+
* {@link NOTIFICATION_EVENTS} and nothing else. The users screen renders a
|
|
8
|
+
* checkbox per entry, `data/users.json` stores whatever is not the default,
|
|
9
|
+
* and the feature that sends asks {@link notificationRecipients} who wants it.
|
|
10
|
+
* Nothing else has to be edited, and nothing anywhere hard-codes the word
|
|
11
|
+
* "comments".
|
|
12
|
+
*
|
|
13
|
+
* Two rules keep a stored preference honest across versions:
|
|
14
|
+
*
|
|
15
|
+
* - **A default rather than a value.** An event a user has said nothing about
|
|
16
|
+
* is at its default, so a site that upgrades into a new notice gets it
|
|
17
|
+
* without anybody visiting the users screen, and turning one off is the only
|
|
18
|
+
* thing that has to be written down.
|
|
19
|
+
* - **An unknown name is not a preference.** A key for an event this version
|
|
20
|
+
* has never heard of is dropped on the way in and on the way out, so a file
|
|
21
|
+
* written by a newer version does not leave a switch nothing can reach.
|
|
22
|
+
*
|
|
23
|
+
* An event may also say that it can be *batched* (TASK-60), which adds a
|
|
24
|
+
* second question beside the switch: not whether to send it, but how often. The
|
|
25
|
+
* two are separate on purpose — a user who wants none of a notice turns it off,
|
|
26
|
+
* and a user who wants it in one message a day is still a recipient. Both
|
|
27
|
+
* rules above apply to the mode exactly as they do to the switch: the default
|
|
28
|
+
* is not written down, and a mode this version has never heard of is dropped
|
|
29
|
+
* and read as the default.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* How often a notice that can be batched arrives.
|
|
33
|
+
*
|
|
34
|
+
* `immediately` is one message per thing that happened, which is what every
|
|
35
|
+
* notice did before there was a choice. The other two are one message per
|
|
36
|
+
* window listing everything still waiting, which is what makes a spam wave a
|
|
37
|
+
* single email rather than fifty.
|
|
38
|
+
*/
|
|
39
|
+
export declare const NOTIFICATION_DELIVERY_MODES: readonly ['immediately', 'hourly', 'daily'];
|
|
40
|
+
/** One of {@link NOTIFICATION_DELIVERY_MODES}. */
|
|
41
|
+
export type NotificationDeliveryMode = (typeof NOTIFICATION_DELIVERY_MODES)[number];
|
|
42
|
+
/**
|
|
43
|
+
* What a user who has never chosen gets: the behaviour every notice had before
|
|
44
|
+
* this existed, so upgrading changes nothing for anybody.
|
|
45
|
+
*/
|
|
46
|
+
export declare const DEFAULT_DELIVERY_MODE: NotificationDeliveryMode;
|
|
47
|
+
/**
|
|
48
|
+
* How long a batched mode waits between messages.
|
|
49
|
+
*
|
|
50
|
+
* `immediately` is zero because it never waits: it is here so that the map is
|
|
51
|
+
* total and nothing has to special-case the mode that does not batch.
|
|
52
|
+
*/
|
|
53
|
+
export declare const DELIVERY_WINDOW_MS: Readonly<Record<NotificationDeliveryMode, number>>;
|
|
54
|
+
/** What the users screen calls each mode. */
|
|
55
|
+
export declare const DELIVERY_MODE_LABELS: Readonly<Record<NotificationDeliveryMode, string>>;
|
|
56
|
+
/** Whether a mode collects things up rather than sending each one. */
|
|
57
|
+
export declare function isBatchedMode(mode: NotificationDeliveryMode): boolean;
|
|
58
|
+
/** One thing a user can be told about. */
|
|
59
|
+
export interface NotificationEvent {
|
|
60
|
+
/** What it is called in `data/users.json` and in the form. */
|
|
61
|
+
readonly name: string;
|
|
62
|
+
/** What the users screen calls it. */
|
|
63
|
+
readonly label: string;
|
|
64
|
+
/** The sentence under the label, so a switch says what it will send. */
|
|
65
|
+
readonly description: string;
|
|
66
|
+
/** Whether a user who has said nothing gets it. */
|
|
67
|
+
readonly byDefault: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Whether a user may choose how often it arrives (TASK-60).
|
|
70
|
+
*
|
|
71
|
+
* `false` for a notice that is only ever one message about one thing — a
|
|
72
|
+
* password having been changed, say — where "hourly" would mean nothing. An
|
|
73
|
+
* event that says `true` gets a delivery mode beside its switch, and
|
|
74
|
+
* whatever sends it is responsible for honouring one.
|
|
75
|
+
*/
|
|
76
|
+
readonly batched: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Every event, in the order the users screen offers them.
|
|
80
|
+
*
|
|
81
|
+
* One line each. TASK-55 ships the first.
|
|
82
|
+
*/
|
|
83
|
+
export declare const NOTIFICATION_EVENTS: readonly NotificationEvent[];
|
|
84
|
+
/** The event of that name, or `undefined` for one nothing here knows. */
|
|
85
|
+
export declare function notificationEvent(name: string): NotificationEvent | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Whether this user wants that notice.
|
|
88
|
+
*
|
|
89
|
+
* `false` for an event nothing knows about, and for no user at all, so a
|
|
90
|
+
* caller can hand in whatever it has.
|
|
91
|
+
*/
|
|
92
|
+
export declare function notificationWanted(user: User | undefined, name: string): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* How often this user wants that notice.
|
|
95
|
+
*
|
|
96
|
+
* {@link DEFAULT_DELIVERY_MODE} for an event nothing knows about, for one that
|
|
97
|
+
* cannot be batched, for no user at all, and for a stored value this version
|
|
98
|
+
* does not recognise — so a caller can hand in whatever it has and always gets
|
|
99
|
+
* a mode it can act on.
|
|
100
|
+
*/
|
|
101
|
+
export declare function notificationMode(user: User | undefined, name: string): NotificationDeliveryMode;
|
|
102
|
+
/** That string as a mode, or `undefined` for one this version has never heard of. */
|
|
103
|
+
export declare function deliveryMode(value: unknown): NotificationDeliveryMode | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Everybody who should get one message of this kind: a user with an address
|
|
106
|
+
* who has not turned it off.
|
|
107
|
+
*
|
|
108
|
+
* The address is what makes somebody a recipient. A user with none has said,
|
|
109
|
+
* by leaving the field empty, that the site has nowhere to write.
|
|
110
|
+
*/
|
|
111
|
+
export declare function notificationRecipients(users: readonly User[], name: string): {
|
|
112
|
+
user: User;
|
|
113
|
+
email: string;
|
|
114
|
+
}[];
|
|
115
|
+
/**
|
|
116
|
+
* The stored map with one switch moved, or `undefined` when nothing needs
|
|
117
|
+
* storing any more.
|
|
118
|
+
*
|
|
119
|
+
* A preference that matches its default is removed rather than written as
|
|
120
|
+
* `true`, so the file says only what somebody actually changed and the default
|
|
121
|
+
* stays the thing this module decides.
|
|
122
|
+
*/
|
|
123
|
+
export declare function withNotification(stored: Readonly<Record<string, boolean>> | undefined, name: string, on: boolean): Record<string, boolean> | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* The stored mode map with one event moved, or `undefined` when nothing needs
|
|
126
|
+
* storing any more.
|
|
127
|
+
*
|
|
128
|
+
* The twin of {@link withNotification}, and it drops for the same three
|
|
129
|
+
* reasons: a mode that matches the default, a mode this version does not know,
|
|
130
|
+
* and an event that cannot be batched are all things the file should not be
|
|
131
|
+
* carrying. The last two are how a value written by hand, or by a newer
|
|
132
|
+
* version, is cleaned up rather than left to sit there meaning nothing.
|
|
133
|
+
*/
|
|
134
|
+
export declare function withNotificationMode(stored: Readonly<Record<string, string>> | undefined, name: string, mode: string): Record<string, string> | undefined;
|
|
135
|
+
/** One event as the users screen renders it: the switch, and the mode when it has one. */
|
|
136
|
+
export interface NotificationSwitch {
|
|
137
|
+
/** The event's name, which is what the form submits. */
|
|
138
|
+
name: string;
|
|
139
|
+
/** What the screen calls it. */
|
|
140
|
+
label: string;
|
|
141
|
+
/** The sentence that says what it will send. */
|
|
142
|
+
description: string;
|
|
143
|
+
/** Whether this user gets it. */
|
|
144
|
+
on: boolean;
|
|
145
|
+
/** Whether it offers a delivery mode at all. */
|
|
146
|
+
batched: boolean;
|
|
147
|
+
/** How often this user gets it. {@link DEFAULT_DELIVERY_MODE} when it does not batch. */
|
|
148
|
+
mode: NotificationDeliveryMode;
|
|
149
|
+
/** The modes to offer, which is nothing at all for an event that cannot batch. */
|
|
150
|
+
modes: {
|
|
151
|
+
value: NotificationDeliveryMode;
|
|
152
|
+
label: string;
|
|
153
|
+
chosen: boolean;
|
|
154
|
+
}[];
|
|
155
|
+
}
|
|
156
|
+
/** Every event with what this user has it set to, for the users screen. */
|
|
157
|
+
export declare function notificationSwitches(user: User): NotificationSwitch[];
|
|
158
|
+
//# sourceMappingURL=preferences.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preferences.d.ts","sourceRoot":"","sources":["../../src/notifications/preferences.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which notices a user wants, as a switchboard rather than a field per notice.
|
|
3
|
+
*
|
|
4
|
+
* The point of this module is that adding the next event — a new follower, a
|
|
5
|
+
* digest of what could not be delivered — is one entry in
|
|
6
|
+
* {@link NOTIFICATION_EVENTS} and nothing else. The users screen renders a
|
|
7
|
+
* checkbox per entry, `data/users.json` stores whatever is not the default,
|
|
8
|
+
* and the feature that sends asks {@link notificationRecipients} who wants it.
|
|
9
|
+
* Nothing else has to be edited, and nothing anywhere hard-codes the word
|
|
10
|
+
* "comments".
|
|
11
|
+
*
|
|
12
|
+
* Two rules keep a stored preference honest across versions:
|
|
13
|
+
*
|
|
14
|
+
* - **A default rather than a value.** An event a user has said nothing about
|
|
15
|
+
* is at its default, so a site that upgrades into a new notice gets it
|
|
16
|
+
* without anybody visiting the users screen, and turning one off is the only
|
|
17
|
+
* thing that has to be written down.
|
|
18
|
+
* - **An unknown name is not a preference.** A key for an event this version
|
|
19
|
+
* has never heard of is dropped on the way in and on the way out, so a file
|
|
20
|
+
* written by a newer version does not leave a switch nothing can reach.
|
|
21
|
+
*
|
|
22
|
+
* An event may also say that it can be *batched* (TASK-60), which adds a
|
|
23
|
+
* second question beside the switch: not whether to send it, but how often. The
|
|
24
|
+
* two are separate on purpose — a user who wants none of a notice turns it off,
|
|
25
|
+
* and a user who wants it in one message a day is still a recipient. Both
|
|
26
|
+
* rules above apply to the mode exactly as they do to the switch: the default
|
|
27
|
+
* is not written down, and a mode this version has never heard of is dropped
|
|
28
|
+
* and read as the default.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* How often a notice that can be batched arrives.
|
|
32
|
+
*
|
|
33
|
+
* `immediately` is one message per thing that happened, which is what every
|
|
34
|
+
* notice did before there was a choice. The other two are one message per
|
|
35
|
+
* window listing everything still waiting, which is what makes a spam wave a
|
|
36
|
+
* single email rather than fifty.
|
|
37
|
+
*/
|
|
38
|
+
export const NOTIFICATION_DELIVERY_MODES = ['immediately', 'hourly', 'daily'];
|
|
39
|
+
/**
|
|
40
|
+
* What a user who has never chosen gets: the behaviour every notice had before
|
|
41
|
+
* this existed, so upgrading changes nothing for anybody.
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_DELIVERY_MODE = 'immediately';
|
|
44
|
+
/**
|
|
45
|
+
* How long a batched mode waits between messages.
|
|
46
|
+
*
|
|
47
|
+
* `immediately` is zero because it never waits: it is here so that the map is
|
|
48
|
+
* total and nothing has to special-case the mode that does not batch.
|
|
49
|
+
*/
|
|
50
|
+
export const DELIVERY_WINDOW_MS = {
|
|
51
|
+
immediately: 0,
|
|
52
|
+
hourly: 60 * 60 * 1000,
|
|
53
|
+
daily: 24 * 60 * 60 * 1000,
|
|
54
|
+
};
|
|
55
|
+
/** What the users screen calls each mode. */
|
|
56
|
+
export const DELIVERY_MODE_LABELS = {
|
|
57
|
+
immediately: 'As they arrive',
|
|
58
|
+
hourly: 'Hourly digest',
|
|
59
|
+
daily: 'Daily digest',
|
|
60
|
+
};
|
|
61
|
+
/** Whether a mode collects things up rather than sending each one. */
|
|
62
|
+
export function isBatchedMode(mode) {
|
|
63
|
+
return DELIVERY_WINDOW_MS[mode] > 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Every event, in the order the users screen offers them.
|
|
67
|
+
*
|
|
68
|
+
* One line each. TASK-55 ships the first.
|
|
69
|
+
*/
|
|
70
|
+
export const NOTIFICATION_EVENTS = [
|
|
71
|
+
{
|
|
72
|
+
name: 'comments',
|
|
73
|
+
label: 'New comments',
|
|
74
|
+
description: 'A comment or a webmention has arrived and is waiting to be approved. The message carries ' +
|
|
75
|
+
'approve, spam and delete links that work without signing in.',
|
|
76
|
+
byDefault: true,
|
|
77
|
+
batched: true,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
/** The event of that name, or `undefined` for one nothing here knows. */
|
|
81
|
+
export function notificationEvent(name) {
|
|
82
|
+
return NOTIFICATION_EVENTS.find((event) => event.name === name);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether this user wants that notice.
|
|
86
|
+
*
|
|
87
|
+
* `false` for an event nothing knows about, and for no user at all, so a
|
|
88
|
+
* caller can hand in whatever it has.
|
|
89
|
+
*/
|
|
90
|
+
export function notificationWanted(user, name) {
|
|
91
|
+
const event = notificationEvent(name);
|
|
92
|
+
if (user === undefined || event === undefined)
|
|
93
|
+
return false;
|
|
94
|
+
const stored = user.notifications?.[name];
|
|
95
|
+
return stored ?? event.byDefault;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* How often this user wants that notice.
|
|
99
|
+
*
|
|
100
|
+
* {@link DEFAULT_DELIVERY_MODE} for an event nothing knows about, for one that
|
|
101
|
+
* cannot be batched, for no user at all, and for a stored value this version
|
|
102
|
+
* does not recognise — so a caller can hand in whatever it has and always gets
|
|
103
|
+
* a mode it can act on.
|
|
104
|
+
*/
|
|
105
|
+
export function notificationMode(user, name) {
|
|
106
|
+
const event = notificationEvent(name);
|
|
107
|
+
if (user === undefined || event === undefined || !event.batched)
|
|
108
|
+
return DEFAULT_DELIVERY_MODE;
|
|
109
|
+
return deliveryMode(user.notificationModes?.[name]) ?? DEFAULT_DELIVERY_MODE;
|
|
110
|
+
}
|
|
111
|
+
/** That string as a mode, or `undefined` for one this version has never heard of. */
|
|
112
|
+
export function deliveryMode(value) {
|
|
113
|
+
return NOTIFICATION_DELIVERY_MODES.find((mode) => mode === value);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Everybody who should get one message of this kind: a user with an address
|
|
117
|
+
* who has not turned it off.
|
|
118
|
+
*
|
|
119
|
+
* The address is what makes somebody a recipient. A user with none has said,
|
|
120
|
+
* by leaving the field empty, that the site has nowhere to write.
|
|
121
|
+
*/
|
|
122
|
+
export function notificationRecipients(users, name) {
|
|
123
|
+
const wanted = [];
|
|
124
|
+
for (const user of users) {
|
|
125
|
+
if (user.email === undefined || user.email === '')
|
|
126
|
+
continue;
|
|
127
|
+
if (!notificationWanted(user, name))
|
|
128
|
+
continue;
|
|
129
|
+
wanted.push({ user, email: user.email });
|
|
130
|
+
}
|
|
131
|
+
return wanted;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The stored map with one switch moved, or `undefined` when nothing needs
|
|
135
|
+
* storing any more.
|
|
136
|
+
*
|
|
137
|
+
* A preference that matches its default is removed rather than written as
|
|
138
|
+
* `true`, so the file says only what somebody actually changed and the default
|
|
139
|
+
* stays the thing this module decides.
|
|
140
|
+
*/
|
|
141
|
+
export function withNotification(stored, name, on) {
|
|
142
|
+
const event = notificationEvent(name);
|
|
143
|
+
if (event === undefined)
|
|
144
|
+
return stored === undefined ? undefined : { ...stored };
|
|
145
|
+
const next = { ...stored };
|
|
146
|
+
if (on === event.byDefault)
|
|
147
|
+
delete next[name];
|
|
148
|
+
else
|
|
149
|
+
next[name] = on;
|
|
150
|
+
return Object.keys(next).length === 0 ? undefined : next;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The stored mode map with one event moved, or `undefined` when nothing needs
|
|
154
|
+
* storing any more.
|
|
155
|
+
*
|
|
156
|
+
* The twin of {@link withNotification}, and it drops for the same three
|
|
157
|
+
* reasons: a mode that matches the default, a mode this version does not know,
|
|
158
|
+
* and an event that cannot be batched are all things the file should not be
|
|
159
|
+
* carrying. The last two are how a value written by hand, or by a newer
|
|
160
|
+
* version, is cleaned up rather than left to sit there meaning nothing.
|
|
161
|
+
*/
|
|
162
|
+
export function withNotificationMode(stored, name, mode) {
|
|
163
|
+
const event = notificationEvent(name);
|
|
164
|
+
const wanted = deliveryMode(mode);
|
|
165
|
+
if (event === undefined || !event.batched)
|
|
166
|
+
return stored === undefined ? undefined : { ...stored };
|
|
167
|
+
const next = { ...stored };
|
|
168
|
+
if (wanted === undefined || wanted === DEFAULT_DELIVERY_MODE)
|
|
169
|
+
delete next[name];
|
|
170
|
+
else
|
|
171
|
+
next[name] = wanted;
|
|
172
|
+
return Object.keys(next).length === 0 ? undefined : next;
|
|
173
|
+
}
|
|
174
|
+
/** Every event with what this user has it set to, for the users screen. */
|
|
175
|
+
export function notificationSwitches(user) {
|
|
176
|
+
return NOTIFICATION_EVENTS.map((event) => {
|
|
177
|
+
const mode = notificationMode(user, event.name);
|
|
178
|
+
return {
|
|
179
|
+
name: event.name,
|
|
180
|
+
label: event.label,
|
|
181
|
+
description: event.description,
|
|
182
|
+
on: notificationWanted(user, event.name),
|
|
183
|
+
batched: event.batched,
|
|
184
|
+
mode,
|
|
185
|
+
modes: event.batched
|
|
186
|
+
? NOTIFICATION_DELIVERY_MODES.map((candidate) => ({
|
|
187
|
+
value: candidate,
|
|
188
|
+
label: DELIVERY_MODE_LABELS[candidate],
|
|
189
|
+
chosen: candidate === mode,
|
|
190
|
+
}))
|
|
191
|
+
: [],
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=preferences.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preferences.js","sourceRoot":"","sources":["../../src/notifications/preferences.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import type { GeekityEnv } from '../env.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Where the links in a notification land.
|
|
5
|
+
*
|
|
6
|
+
* Two endpoints under `/_geekity/`, and neither has a session behind it: the
|
|
7
|
+
* token in the URL is the whole of the authority, which is the point — a
|
|
8
|
+
* moderator reading mail on a phone should not have to sign in to approve a
|
|
9
|
+
* comment, and somebody unsubscribing has no account to sign in to.
|
|
10
|
+
*
|
|
11
|
+
* **Opening a link does nothing.** Every link lands on a page with a button on
|
|
12
|
+
* it, and only pressing the button acts. That is not politeness: mail readers,
|
|
13
|
+
* spam filters and link-safety scanners fetch the URLs in a message as a
|
|
14
|
+
* matter of course, and a GET that moderated would mean a corporate mail
|
|
15
|
+
* gateway silently deleting this site's comments. The button posts the same
|
|
16
|
+
* token back, and there is no CSRF field on it because there is no session to
|
|
17
|
+
* confuse — anybody who could make a browser post this form already has the
|
|
18
|
+
* token, which is everything.
|
|
19
|
+
*
|
|
20
|
+
* **A moderation link works once.** The token's signature is checked, and then
|
|
21
|
+
* it is spent against the cache before anything is written, so a link forwarded
|
|
22
|
+
* to somebody else is a link that has already been used. An unsubscribe link is
|
|
23
|
+
* deliberately not spent: clicking it a second time should say "you are
|
|
24
|
+
* unsubscribed", not "that link is dead".
|
|
25
|
+
*/
|
|
26
|
+
/** Register both landing pages. Mounted by the public site. */
|
|
27
|
+
export declare function mountNotificationLinks(app: Hono<GeekityEnv>): void;
|
|
28
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/notifications/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,MAAM,CAAC;AAI1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAU5C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,+DAA+D;AAC/D,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAMlE"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { isModerationAction, moderateComment } from '../comments/moderate.js';
|
|
2
|
+
import { MODERATE_PATH, NOTIFICATION_FIELDS, UNSUBSCRIBE_ACTION, UNSUBSCRIBE_PATH, } from './links.js';
|
|
3
|
+
import { addCommentOptOut, hasOptedOut } from './optouts.js';
|
|
4
|
+
import { notificationTokenExpiry, readNotificationToken } from './tokens.js';
|
|
5
|
+
/**
|
|
6
|
+
* Where the links in a notification land.
|
|
7
|
+
*
|
|
8
|
+
* Two endpoints under `/_geekity/`, and neither has a session behind it: the
|
|
9
|
+
* token in the URL is the whole of the authority, which is the point — a
|
|
10
|
+
* moderator reading mail on a phone should not have to sign in to approve a
|
|
11
|
+
* comment, and somebody unsubscribing has no account to sign in to.
|
|
12
|
+
*
|
|
13
|
+
* **Opening a link does nothing.** Every link lands on a page with a button on
|
|
14
|
+
* it, and only pressing the button acts. That is not politeness: mail readers,
|
|
15
|
+
* spam filters and link-safety scanners fetch the URLs in a message as a
|
|
16
|
+
* matter of course, and a GET that moderated would mean a corporate mail
|
|
17
|
+
* gateway silently deleting this site's comments. The button posts the same
|
|
18
|
+
* token back, and there is no CSRF field on it because there is no session to
|
|
19
|
+
* confuse — anybody who could make a browser post this form already has the
|
|
20
|
+
* token, which is everything.
|
|
21
|
+
*
|
|
22
|
+
* **A moderation link works once.** The token's signature is checked, and then
|
|
23
|
+
* it is spent against the cache before anything is written, so a link forwarded
|
|
24
|
+
* to somebody else is a link that has already been used. An unsubscribe link is
|
|
25
|
+
* deliberately not spent: clicking it a second time should say "you are
|
|
26
|
+
* unsubscribed", not "that link is dead".
|
|
27
|
+
*/
|
|
28
|
+
/** Register both landing pages. Mounted by the public site. */
|
|
29
|
+
export function mountNotificationLinks(app) {
|
|
30
|
+
app.get(MODERATE_PATH, async (c) => await moderationLanding(c, false));
|
|
31
|
+
app.post(MODERATE_PATH, async (c) => await moderationLanding(c, true));
|
|
32
|
+
app.get(UNSUBSCRIBE_PATH, async (c) => await unsubscribeLanding(c, false));
|
|
33
|
+
app.post(UNSUBSCRIBE_PATH, async (c) => await unsubscribeLanding(c, true));
|
|
34
|
+
}
|
|
35
|
+
/** The moderation link: the confirm page, and the thing it confirms. */
|
|
36
|
+
async function moderationLanding(c, act) {
|
|
37
|
+
const { config, admin } = c.var;
|
|
38
|
+
const submitted = act ? await c.req.parseBody() : {};
|
|
39
|
+
const token = act ? field(submitted[NOTIFICATION_FIELDS.token]) : (c.req.query('token') ?? '');
|
|
40
|
+
const named = act ? field(submitted[NOTIFICATION_FIELDS.action]) : (c.req.query('action') ?? '');
|
|
41
|
+
const claim = readNotificationToken(config.dataDir, token, config.now());
|
|
42
|
+
// The named action has to be the signed one. It is in the query so a person
|
|
43
|
+
// can read what a link does; if the two disagree, the link has been edited.
|
|
44
|
+
if (claim === undefined || claim.action !== named || !isModerationAction(claim.action)) {
|
|
45
|
+
return refuse(c, 'This link is no longer valid. Sign in to the Comments screen instead.');
|
|
46
|
+
}
|
|
47
|
+
const action = claim.action;
|
|
48
|
+
const comment = admin.getComment(claim.subject);
|
|
49
|
+
if (!act) {
|
|
50
|
+
if (comment === undefined)
|
|
51
|
+
return done(c, 'That comment is not here any more.');
|
|
52
|
+
return confirm(c, {
|
|
53
|
+
action,
|
|
54
|
+
token,
|
|
55
|
+
heading: `${verb(action)} this comment?`,
|
|
56
|
+
author: comment.author.name,
|
|
57
|
+
html: comment.content.html,
|
|
58
|
+
button: verb(action),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Spent before anything is written, so two clicks cannot both act, and a
|
|
62
|
+
// forwarded link is a link that has already been used.
|
|
63
|
+
const expiry = notificationTokenExpiry(config.dataDir, token);
|
|
64
|
+
if (expiry === undefined || !admin.spendToken(token, expiry, config.now())) {
|
|
65
|
+
return done(c, 'That link has already been used. Sign in to the Comments screen instead.');
|
|
66
|
+
}
|
|
67
|
+
// A good moment to sweep: this is the one handler that writes such a row.
|
|
68
|
+
admin.pruneSpentTokens(config.now());
|
|
69
|
+
const outcome = await moderateComment({
|
|
70
|
+
records: { admin, contentDir: config.contentDir },
|
|
71
|
+
id: claim.subject,
|
|
72
|
+
action,
|
|
73
|
+
checker: config.commentChecker,
|
|
74
|
+
baseUrl: config.baseUrl,
|
|
75
|
+
});
|
|
76
|
+
if (outcome.kind === 'gone')
|
|
77
|
+
return done(c, 'That comment is not here any more.');
|
|
78
|
+
if (outcome.kind === 'unchanged') {
|
|
79
|
+
return done(c, `${outcome.comment.author.name}’s comment was already ${was(action)}.`);
|
|
80
|
+
}
|
|
81
|
+
// A newly approved reply is what somebody upthread asked to hear about.
|
|
82
|
+
if (outcome.kind === 'moved' && outcome.comment.status === 'approved') {
|
|
83
|
+
c.var.notifications.replyApproved(outcome.comment);
|
|
84
|
+
}
|
|
85
|
+
return done(c, `${outcome.comment.author.name}’s comment has been ${was(action)}.`);
|
|
86
|
+
}
|
|
87
|
+
/** The unsubscribe link: the confirm page, and the thing it confirms. */
|
|
88
|
+
async function unsubscribeLanding(c, act) {
|
|
89
|
+
const { config } = c.var;
|
|
90
|
+
const submitted = act ? await c.req.parseBody() : {};
|
|
91
|
+
const token = act ? field(submitted[NOTIFICATION_FIELDS.token]) : (c.req.query('token') ?? '');
|
|
92
|
+
const claim = readNotificationToken(config.dataDir, token, config.now());
|
|
93
|
+
if (claim === undefined || claim.action !== UNSUBSCRIBE_ACTION) {
|
|
94
|
+
return refuse(c, 'This link is no longer valid.');
|
|
95
|
+
}
|
|
96
|
+
if (!act) {
|
|
97
|
+
if (hasOptedOut(config.dataDir, claim.subject)) {
|
|
98
|
+
return done(c, `${claim.subject} is not being emailed about replies.`);
|
|
99
|
+
}
|
|
100
|
+
return confirm(c, {
|
|
101
|
+
action: UNSUBSCRIBE_ACTION,
|
|
102
|
+
token,
|
|
103
|
+
heading: 'Stop emailing you about replies?',
|
|
104
|
+
author: claim.subject,
|
|
105
|
+
html: `<p>Nothing more will be sent to ${escapeHtml(claim.subject)} about replies to comments on this site. Your comments stay where they are.</p>`,
|
|
106
|
+
button: 'Unsubscribe',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
await addCommentOptOut(config.dataDir, claim.subject);
|
|
110
|
+
return done(c, `${claim.subject} will not be emailed about replies again.`);
|
|
111
|
+
}
|
|
112
|
+
/** The verb a button and a heading use for one action. */
|
|
113
|
+
function verb(action) {
|
|
114
|
+
return action === 'approve' ? 'Approve' : action === 'spam' ? 'Mark as spam' : 'Delete';
|
|
115
|
+
}
|
|
116
|
+
/** What that action left behind, as a sentence says it. */
|
|
117
|
+
function was(action) {
|
|
118
|
+
return action === 'approve' ? 'approved' : action === 'spam' ? 'filed as spam' : 'deleted';
|
|
119
|
+
}
|
|
120
|
+
/** The page a link lands on: what it would do, and a button that does it. */
|
|
121
|
+
function confirm(c, options) {
|
|
122
|
+
return c.html(page(options.heading, `<h1>${escapeHtml(options.heading)}</h1>
|
|
123
|
+
<p class="who">${escapeHtml(options.author)}</p>
|
|
124
|
+
<blockquote>${options.html}</blockquote>
|
|
125
|
+
<form method="post">
|
|
126
|
+
<input type="hidden" name="${NOTIFICATION_FIELDS.action}" value="${escapeHtml(options.action)}" />
|
|
127
|
+
<input type="hidden" name="${NOTIFICATION_FIELDS.token}" value="${escapeHtml(options.token)}" />
|
|
128
|
+
<button type="submit">${escapeHtml(options.button)}</button>
|
|
129
|
+
</form>`));
|
|
130
|
+
}
|
|
131
|
+
/** The page a finished action leaves behind. */
|
|
132
|
+
function done(c, message) {
|
|
133
|
+
return c.html(page(message, `<h1>${escapeHtml(message)}</h1>`));
|
|
134
|
+
}
|
|
135
|
+
/** The page a link that buys nothing lands on. */
|
|
136
|
+
function refuse(c, message) {
|
|
137
|
+
c.status(400);
|
|
138
|
+
return c.html(page(message, `<h1>${escapeHtml(message)}</h1>`));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* One whole HTML document, built here rather than through the theme.
|
|
142
|
+
*
|
|
143
|
+
* These two pages are the CMS's own, like the health check: they are reached
|
|
144
|
+
* from an email by somebody who may never have seen the site, they have to
|
|
145
|
+
* work on a theme that has been half rewritten, and there is nothing about
|
|
146
|
+
* them a theme would want to say. Deliberately one file, no assets and no
|
|
147
|
+
* script, so nothing about them can fail to load.
|
|
148
|
+
*/
|
|
149
|
+
function page(title, body) {
|
|
150
|
+
return `<!doctype html>
|
|
151
|
+
<html lang="en">
|
|
152
|
+
<head>
|
|
153
|
+
<meta charset="utf-8" />
|
|
154
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
155
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
156
|
+
<title>${escapeHtml(title)}</title>
|
|
157
|
+
<style>
|
|
158
|
+
body { font: 16px/1.5 system-ui, sans-serif; margin: 0; padding: 2rem 1rem; max-width: 40rem; }
|
|
159
|
+
h1 { font-size: 1.3rem; }
|
|
160
|
+
.who { color: #555; margin: 0 0 .5rem; }
|
|
161
|
+
blockquote { border-left: 3px solid #ccc; margin: 0 0 1.5rem; padding: 0 0 0 1rem; }
|
|
162
|
+
button { font: inherit; padding: .5rem 1rem; }
|
|
163
|
+
</style>
|
|
164
|
+
</head>
|
|
165
|
+
<body>
|
|
166
|
+
${body}
|
|
167
|
+
</body>
|
|
168
|
+
</html>
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
/** Text as it may safely be printed into the page. */
|
|
172
|
+
function escapeHtml(value) {
|
|
173
|
+
return value
|
|
174
|
+
.replaceAll('&', '&')
|
|
175
|
+
.replaceAll('<', '<')
|
|
176
|
+
.replaceAll('>', '>')
|
|
177
|
+
.replaceAll('"', '"')
|
|
178
|
+
.replaceAll("'", ''');
|
|
179
|
+
}
|
|
180
|
+
/** A form field as a string. A file upload, or a missing field, is the empty one. */
|
|
181
|
+
function field(value) {
|
|
182
|
+
return typeof value === 'string' ? value : '';
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/notifications/routes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,+DAA+D;AAC/D,MAAM,UAAU,sBAAsB,CAAC,GAAqB;IAC1D,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,iBAAiB,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IACvE,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,iBAAiB,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;IAEvE,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,kBAAkB,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAC3E,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,kBAAkB,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,wEAAwE;AACxE,KAAK,UAAU,iBAAiB,CAAC,CAAsB,EAAE,GAAY;IACnE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC;IAChC,MAAM,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACrD,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/F,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAEjG,MAAM,KAAK,GAAG,qBAAqB,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IACzE,4EAA4E;IAC5E,4EAA4E;IAC5E,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACvF,OAAO,MAAM,CAAC,CAAC,EAAE,uEAAuE,CAAC,CAAC;IAC5F,CAAC;IAED,MAAM,MAAM,GAAqB,KAAK,CAAC,MAAM,CAAC;IAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAEhD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,CAAC,EAAE,oCAAoC,CAAC,CAAC;QAChF,OAAO,OAAO,CAAC,CAAC,EAAE;YAChB,MAAM;YACN,KAAK;YACL,OAAO,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,gBAAgB;YACxC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;YAC3B,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI;YAC1B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;SACrB,CAAC,CAAC;IACL,CAAC;IAED,yEAAyE;IACzE,uDAAuD;IACvD,MAAM,MAAM,GAAG,uBAAuB,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC9D,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;QAC3E,OAAO,IAAI,CAAC,CAAC,EAAE,0EAA0E,CAAC,CAAC;IAC7F,CAAC;IACD,0EAA0E;IAC1E,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IAErC,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC;QACpC,OAAO,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE;QACjD,EAAE,EAAE,KAAK,CAAC,OAAO;QACjB,MAAM;QACN,OAAO,EAAE,MAAM,CAAC,cAAc;QAC9B,OAAO,EAAE,MAAM,CAAC,OAAO;KACxB,CAAC,CAAC;IAEH,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC,CAAC,EAAE,oCAAoC,CAAC,CAAC;IAClF,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACjC,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,0BAA0B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzF,CAAC;IAED,wEAAwE;IACxE,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACtE,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,uBAAuB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AACtF,CAAC;AAED,yEAAyE;AACzE,KAAK,UAAU,kBAAkB,CAAC,CAAsB,EAAE,GAAY;IACpE,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC;IACzB,MAAM,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACrD,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAE/F,MAAM,KAAK,GAAG,qBAAqB,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IACzE,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,kBAAkB,EAAE,CAAC;QAC/D,OAAO,MAAM,CAAC,CAAC,EAAE,+BAA+B,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,IAAI,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/C,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,OAAO,sCAAsC,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,OAAO,CAAC,CAAC,EAAE;YAChB,MAAM,EAAE,kBAAkB;YAC1B,KAAK;YACL,OAAO,EAAE,kCAAkC;YAC3C,MAAM,EAAE,KAAK,CAAC,OAAO;YACrB,IAAI,EAAE,mCAAmC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,iFAAiF;YACnJ,MAAM,EAAE,aAAa;SACtB,CAAC,CAAC;IACL,CAAC;IAED,MAAM,gBAAgB,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACtD,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,OAAO,2CAA2C,CAAC,CAAC;AAC9E,CAAC;AAED,0DAA0D;AAC1D,SAAS,IAAI,CAAC,MAAwB;IACpC,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC1F,CAAC;AAED,2DAA2D;AAC3D,SAAS,GAAG,CAAC,MAAwB;IACnC,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7F,CAAC;AAED,6EAA6E;AAC7E,SAAS,OAAO,CACd,CAAsB,EACtB,OAOC;IAED,OAAO,CAAC,CAAC,IAAI,CACX,IAAI,CACF,OAAO,CAAC,OAAO,EACf,OAAO,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC;uBACjB,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;oBAC7B,OAAO,CAAC,IAAI;;qCAEK,mBAAmB,CAAC,MAAM,YAAY,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;qCAChE,mBAAmB,CAAC,KAAK,YAAY,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC;gCACnE,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;cAC5C,CACT,CACF,CAAC;AACJ,CAAC;AAED,gDAAgD;AAChD,SAAS,IAAI,CAAC,CAAsB,EAAE,OAAe;IACnD,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,kDAAkD;AAClD,SAAS,MAAM,CAAC,CAAsB,EAAE,OAAe;IACrD,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACd,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,IAAI,CAAC,KAAa,EAAE,IAAY;IACvC,OAAO;;;;;;SAMA,UAAU,CAAC,KAAK,CAAC;;;;;;;;;;EAUxB,IAAI;;;CAGL,CAAC;AACF,CAAC;AAED,sDAAsD;AACtD,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC;SACzB,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAC9B,CAAC;AAED,qFAAqF;AACrF,SAAS,KAAK,CAAC,KAAc;IAC3B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thing that makes a link in an email work without a login.
|
|
3
|
+
*
|
|
4
|
+
* A notification is only worth sending if acting on it is one click. That
|
|
5
|
+
* means the link itself has to carry the authority, and the two ways of doing
|
|
6
|
+
* that are a random string written down somewhere and a signed claim that
|
|
7
|
+
* needs nothing written down. This is the second, for one reason: a link lives
|
|
8
|
+
* in an inbox for a week, and `data/geekity.db` is a cache that a site may
|
|
9
|
+
* legitimately delete, rebuild or restore from a backup at any moment
|
|
10
|
+
* (decision-9). A stack of rows would take every link in flight with it.
|
|
11
|
+
*
|
|
12
|
+
* So the token says what it is for — approve *this* comment, unsubscribe
|
|
13
|
+
* *this* address — and carries an HMAC over exactly that, taken with a secret
|
|
14
|
+
* in `data/notification-secret`. Nothing but the secret is needed to check
|
|
15
|
+
* one, the claim cannot be edited without breaking the signature, and a token
|
|
16
|
+
* signed by one site means nothing to another.
|
|
17
|
+
*
|
|
18
|
+
* What is *not* in the token is the right to be used twice. That is a row in
|
|
19
|
+
* the cache (`spent_tokens`), and it is deliberately the half that a lost
|
|
20
|
+
* database forgets: forgetting that a link was spent re-enables an action that
|
|
21
|
+
* has already happened and is idempotent — approving an approved comment, or
|
|
22
|
+
* deleting one that is gone — whereas forgetting the secret would break every
|
|
23
|
+
* link a site had sent.
|
|
24
|
+
*/
|
|
25
|
+
/** Where the signing secret lives, under `dataDir`. */
|
|
26
|
+
export declare const NOTIFICATION_SECRET_FILE = "notification-secret";
|
|
27
|
+
/** What a link is allowed to do, once its signature checks out. */
|
|
28
|
+
export interface NotificationClaim {
|
|
29
|
+
/** Which one-click action this link performs. */
|
|
30
|
+
action: string;
|
|
31
|
+
/**
|
|
32
|
+
* What it acts on: a comment's id for the moderation links, an email address
|
|
33
|
+
* for an unsubscribe.
|
|
34
|
+
*/
|
|
35
|
+
subject: string;
|
|
36
|
+
}
|
|
37
|
+
/** A claim on its way into a link, with how long the link should last. */
|
|
38
|
+
export interface NewNotificationToken extends NotificationClaim {
|
|
39
|
+
/** How long it works for, in seconds. */
|
|
40
|
+
lifetimeSeconds: number;
|
|
41
|
+
}
|
|
42
|
+
/** Mint a signed link token. It is the whole of what the link carries. */
|
|
43
|
+
export declare function signNotificationToken(dataDir: string, claim: NewNotificationToken, now?: Date): string;
|
|
44
|
+
/**
|
|
45
|
+
* What a token entitles its holder to, or `undefined` when it entitles them to
|
|
46
|
+
* nothing: not a token at all, edited, signed by somebody else, or expired.
|
|
47
|
+
*
|
|
48
|
+
* The signature is checked before the payload is believed, so nothing here
|
|
49
|
+
* ever acts on bytes a stranger chose.
|
|
50
|
+
*/
|
|
51
|
+
export declare function readNotificationToken(dataDir: string, token: string, now?: Date): NotificationClaim | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* When a token stops working, as an ISO 8601 instant, or `undefined` when it
|
|
54
|
+
* never worked.
|
|
55
|
+
*
|
|
56
|
+
* What the spent-token row is pruned by: a ledger of links that can never be
|
|
57
|
+
* used again is a ledger that can be thrown away.
|
|
58
|
+
*/
|
|
59
|
+
export declare function notificationTokenExpiry(dataDir: string, token: string): string | undefined;
|
|
60
|
+
//# sourceMappingURL=tokens.d.ts.map
|