@morphemecms/cms 0.1.0 → 0.2.0-next-20261104150211
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/CHANGELOG.md +12 -0
- package/LICENSE +21 -21
- package/README.md +51 -51
- package/astro.mjs +162 -162
- package/bin/morpheme.mjs +38 -38
- package/client.d.ts +10 -10
- package/defaults/blockTexts.ts +33 -33
- package/defaults/collections.ts +774 -774
- package/defaults/colors.ts +125 -125
- package/defaults/company.ts +20 -20
- package/defaults/editor.ts +178 -178
- package/defaults/internalLinks.ts +30 -30
- package/defaults/opens.ts +18 -18
- package/defaults/popup.tsx +93 -93
- package/defaults/privacy.ts +79 -79
- package/defaults/rules.ts +450 -450
- package/defaults/schema.ts +16 -16
- package/defaults/sitedata.ts +129 -129
- package/defaults/standard.ts +106 -106
- package/defaults/standardPages.ts +87 -87
- package/defaults/texts.ts +481 -481
- package/demo/README.md +26 -26
- package/demo/src/content/blog/build-a-page-from-blocks.md +19 -19
- package/demo/src/content/blog/colors-everyone-can-read.md +19 -19
- package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -21
- package/demo/src/content/blog/every-save-is-a-version.md +21 -21
- package/demo/src/content/blog/schedule-a-post.md +17 -17
- package/demo/src/content/blog/secure-from-day-one.md +17 -17
- package/demo/src/content/blog/seo-without-the-guesswork.md +23 -23
- package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -23
- package/demo/src/content/blog/the-app-store.md +21 -21
- package/demo/src/content/pages/about.md +22 -22
- package/demo/src/content/pages/app-store.md +30 -30
- package/demo/src/content/pages/blocks.md +98 -98
- package/demo/src/content/pages/blog-page.md +11 -11
- package/demo/src/content/pages/contact.md +16 -16
- package/demo/src/content/pages/get-started.md +20 -20
- package/demo/src/content/pages/home.md +26 -26
- package/demo/src/content/pages/page-not-found.md +11 -11
- package/demo/src/content/pages/privacy-policy.md +24 -24
- package/demo/src/data/blocks.json +25 -25
- package/demo/src/data/booking.json +6 -6
- package/demo/src/data/categories/apps.json +6 -6
- package/demo/src/data/categories/design.json +6 -6
- package/demo/src/data/categories/editing.json +6 -6
- package/demo/src/data/categories/security.json +6 -6
- package/demo/src/data/categories/seo.json +6 -6
- package/demo/src/data/colors.json +75 -75
- package/demo/src/data/contact.json +8 -8
- package/demo/src/data/cookies.json +21 -21
- package/demo/src/data/footer.json +27 -27
- package/demo/src/data/menu.json +35 -35
- package/demo/src/data/mobile-menu.json +25 -25
- package/demo/src/data/popups/booking-bar.json +13 -13
- package/demo/src/data/popups/try-the-editor.json +16 -16
- package/demo/src/data/standard-pages.json +8 -8
- package/demo/src/data/testimonials/sample-bakery.json +10 -10
- package/demo/src/data/testimonials/sample-clinic.json +10 -10
- package/demo/src/data/testimonials/sample-studio.json +10 -10
- package/demo/src/site/colors.ts +11 -11
- package/demo/src/site/config.ts +19 -19
- package/icons/lucide/LICENSE +43 -43
- package/icons/lucide/address-book.svg +1 -1
- package/icons/lucide/app-window.svg +1 -1
- package/icons/lucide/arrow-counter-clockwise.svg +1 -1
- package/icons/lucide/arrow-down.svg +1 -1
- package/icons/lucide/arrow-left.svg +1 -1
- package/icons/lucide/arrow-right.svg +1 -1
- package/icons/lucide/arrow-square-out.svg +1 -1
- package/icons/lucide/arrow-u-up-left.svg +1 -1
- package/icons/lucide/arrow-u-up-right.svg +1 -1
- package/icons/lucide/arrow-up-right.svg +1 -1
- package/icons/lucide/arrow-up.svg +1 -1
- package/icons/lucide/arrows-down-up.svg +1 -1
- package/icons/lucide/arrows-in-line-horizontal.svg +1 -1
- package/icons/lucide/arrows-out-line-horizontal.svg +1 -1
- package/icons/lucide/article.svg +1 -1
- package/icons/lucide/bank.svg +1 -1
- package/icons/lucide/bell.svg +1 -1
- package/icons/lucide/bicycle.svg +1 -1
- package/icons/lucide/binoculars.svg +1 -1
- package/icons/lucide/book-open-text.svg +1 -1
- package/icons/lucide/book-open.svg +1 -1
- package/icons/lucide/browser.svg +1 -1
- package/icons/lucide/buildings.svg +1 -1
- package/icons/lucide/bus.svg +1 -1
- package/icons/lucide/calendar-blank.svg +1 -1
- package/icons/lucide/calendar-check.svg +1 -1
- package/icons/lucide/car.svg +1 -1
- package/icons/lucide/cards.svg +1 -1
- package/icons/lucide/caret-down.svg +1 -1
- package/icons/lucide/caret-left.svg +1 -1
- package/icons/lucide/caret-right.svg +1 -1
- package/icons/lucide/caret-up.svg +1 -1
- package/icons/lucide/certificate.svg +1 -1
- package/icons/lucide/chart-line-up.svg +1 -1
- package/icons/lucide/chats.svg +1 -1
- package/icons/lucide/check-circle.svg +1 -1
- package/icons/lucide/check.svg +1 -1
- package/icons/lucide/circle-half.svg +1 -1
- package/icons/lucide/circle.svg +1 -1
- package/icons/lucide/clock-counter-clockwise.svg +1 -1
- package/icons/lucide/clock.svg +1 -1
- package/icons/lucide/code.svg +1 -1
- package/icons/lucide/coffee.svg +1 -1
- package/icons/lucide/columns.svg +1 -1
- package/icons/lucide/cookie.svg +1 -1
- package/icons/lucide/copy.svg +1 -1
- package/icons/lucide/credit-card.svg +1 -1
- package/icons/lucide/currency-circle-dollar.svg +1 -1
- package/icons/lucide/cursor-click.svg +1 -1
- package/icons/lucide/database.svg +1 -1
- package/icons/lucide/desktop.svg +1 -1
- package/icons/lucide/device-mobile.svg +1 -1
- package/icons/lucide/door-open.svg +1 -1
- package/icons/lucide/download-simple.svg +1 -1
- package/icons/lucide/envelope-simple.svg +1 -1
- package/icons/lucide/eye-slash.svg +1 -1
- package/icons/lucide/eye.svg +1 -1
- package/icons/lucide/file-pdf.svg +1 -1
- package/icons/lucide/file-text.svg +1 -1
- package/icons/lucide/file-video.svg +1 -1
- package/icons/lucide/file-zip.svg +1 -1
- package/icons/lucide/flower-lotus.svg +1 -1
- package/icons/lucide/folder-simple.svg +1 -1
- package/icons/lucide/funnel.svg +1 -1
- package/icons/lucide/gauge.svg +1 -1
- package/icons/lucide/gear.svg +1 -1
- package/icons/lucide/github-logo.svg +1 -1
- package/icons/lucide/globe.svg +1 -1
- package/icons/lucide/google-logo.svg +1 -1
- package/icons/lucide/graduation-cap.svg +1 -1
- package/icons/lucide/hammer.svg +1 -1
- package/icons/lucide/hand-heart.svg +1 -1
- package/icons/lucide/handshake.svg +1 -1
- package/icons/lucide/hard-drives.svg +1 -1
- package/icons/lucide/heart.svg +1 -1
- package/icons/lucide/house.svg +1 -1
- package/icons/lucide/image-square.svg +1 -1
- package/icons/lucide/image.svg +1 -1
- package/icons/lucide/info.svg +1 -1
- package/icons/lucide/keyboard.svg +1 -1
- package/icons/lucide/leaf.svg +1 -1
- package/icons/lucide/lifebuoy.svg +1 -1
- package/icons/lucide/lightbulb.svg +1 -1
- package/icons/lucide/link-break.svg +1 -1
- package/icons/lucide/link-simple.svg +1 -1
- package/icons/lucide/list-bullets.svg +1 -1
- package/icons/lucide/list-checks.svg +1 -1
- package/icons/lucide/list-numbers.svg +1 -1
- package/icons/lucide/list.svg +1 -1
- package/icons/lucide/lock-simple-open.svg +1 -1
- package/icons/lucide/lock-simple.svg +1 -1
- package/icons/lucide/magnifying-glass.svg +1 -1
- package/icons/lucide/map-pin.svg +1 -1
- package/icons/lucide/map-trifold.svg +1 -1
- package/icons/lucide/medal.svg +1 -1
- package/icons/lucide/megaphone.svg +1 -1
- package/icons/lucide/minus.svg +1 -1
- package/icons/lucide/moon.svg +1 -1
- package/icons/lucide/newspaper.svg +1 -1
- package/icons/lucide/pack.json +23 -23
- package/icons/lucide/paint-brush-broad.svg +1 -1
- package/icons/lucide/palette.svg +1 -1
- package/icons/lucide/paper-plane-tilt.svg +1 -1
- package/icons/lucide/pencil-simple.svg +1 -1
- package/icons/lucide/phone.svg +1 -1
- package/icons/lucide/play-fill.svg +1 -1
- package/icons/lucide/plus.svg +1 -1
- package/icons/lucide/prohibit.svg +1 -1
- package/icons/lucide/puzzle-piece.svg +1 -1
- package/icons/lucide/question.svg +1 -1
- package/icons/lucide/quotes.svg +1 -1
- package/icons/lucide/rows.svg +1 -1
- package/icons/lucide/ruler.svg +1 -1
- package/icons/lucide/scan.svg +1 -1
- package/icons/lucide/shapes.svg +1 -1
- package/icons/lucide/share-network.svg +1 -1
- package/icons/lucide/shield-check.svg +1 -1
- package/icons/lucide/sidebar-simple.svg +1 -1
- package/icons/lucide/sign-out.svg +1 -1
- package/icons/lucide/signpost.svg +1 -1
- package/icons/lucide/sliders-horizontal.svg +1 -1
- package/icons/lucide/smiley.svg +1 -1
- package/icons/lucide/sparkle.svg +1 -1
- package/icons/lucide/square-half-bottom.svg +1 -1
- package/icons/lucide/square.svg +1 -1
- package/icons/lucide/squares-four.svg +1 -1
- package/icons/lucide/sticker.svg +1 -1
- package/icons/lucide/storefront.svg +1 -1
- package/icons/lucide/sun.svg +1 -1
- package/icons/lucide/swatches.svg +1 -1
- package/icons/lucide/tag.svg +1 -1
- package/icons/lucide/text-aa.svg +1 -1
- package/icons/lucide/text-align-left.svg +1 -1
- package/icons/lucide/text-b.svg +1 -1
- package/icons/lucide/text-h-one.svg +1 -1
- package/icons/lucide/text-h-three.svg +1 -1
- package/icons/lucide/text-h-two.svg +1 -1
- package/icons/lucide/text-italic.svg +1 -1
- package/icons/lucide/text-t.svg +1 -1
- package/icons/lucide/ticket.svg +1 -1
- package/icons/lucide/toggle-right.svg +1 -1
- package/icons/lucide/train.svg +1 -1
- package/icons/lucide/trash.svg +1 -1
- package/icons/lucide/upload-simple.svg +1 -1
- package/icons/lucide/user-circle.svg +1 -1
- package/icons/lucide/user.svg +1 -1
- package/icons/lucide/users-three.svg +1 -1
- package/icons/lucide/users.svg +1 -1
- package/icons/lucide/video-camera.svg +1 -1
- package/icons/lucide/warning-circle.svg +1 -1
- package/icons/lucide/warning.svg +1 -1
- package/icons/lucide/wave-sine.svg +1 -1
- package/icons/lucide/wheelchair.svg +1 -1
- package/icons/lucide/wrench.svg +1 -1
- package/icons/lucide/x.svg +1 -1
- package/icons/material-symbols/LICENSE +201 -201
- package/icons/material-symbols/address-book.svg +1 -1
- package/icons/material-symbols/app-window.svg +1 -1
- package/icons/material-symbols/arrow-counter-clockwise.svg +1 -1
- package/icons/material-symbols/arrow-down.svg +1 -1
- package/icons/material-symbols/arrow-left.svg +1 -1
- package/icons/material-symbols/arrow-right.svg +1 -1
- package/icons/material-symbols/arrow-square-out.svg +1 -1
- package/icons/material-symbols/arrow-u-up-left.svg +1 -1
- package/icons/material-symbols/arrow-u-up-right.svg +1 -1
- package/icons/material-symbols/arrow-up-right.svg +1 -1
- package/icons/material-symbols/arrow-up.svg +1 -1
- package/icons/material-symbols/arrows-down-up.svg +1 -1
- package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -1
- package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -1
- package/icons/material-symbols/article.svg +1 -1
- package/icons/material-symbols/bank.svg +1 -1
- package/icons/material-symbols/bell.svg +1 -1
- package/icons/material-symbols/bicycle.svg +1 -1
- package/icons/material-symbols/binoculars.svg +1 -1
- package/icons/material-symbols/book-open-text.svg +1 -1
- package/icons/material-symbols/book-open.svg +1 -1
- package/icons/material-symbols/browser.svg +1 -1
- package/icons/material-symbols/buildings.svg +1 -1
- package/icons/material-symbols/bus.svg +1 -1
- package/icons/material-symbols/calendar-blank.svg +1 -1
- package/icons/material-symbols/calendar-check.svg +1 -1
- package/icons/material-symbols/car.svg +1 -1
- package/icons/material-symbols/cards.svg +1 -1
- package/icons/material-symbols/caret-down.svg +1 -1
- package/icons/material-symbols/caret-left.svg +1 -1
- package/icons/material-symbols/caret-right.svg +1 -1
- package/icons/material-symbols/caret-up.svg +1 -1
- package/icons/material-symbols/certificate.svg +1 -1
- package/icons/material-symbols/chart-line-up.svg +1 -1
- package/icons/material-symbols/chats.svg +1 -1
- package/icons/material-symbols/check-circle.svg +1 -1
- package/icons/material-symbols/check.svg +1 -1
- package/icons/material-symbols/circle-half.svg +1 -1
- package/icons/material-symbols/circle.svg +1 -1
- package/icons/material-symbols/clock-counter-clockwise.svg +1 -1
- package/icons/material-symbols/clock.svg +1 -1
- package/icons/material-symbols/code.svg +1 -1
- package/icons/material-symbols/coffee.svg +1 -1
- package/icons/material-symbols/columns.svg +1 -1
- package/icons/material-symbols/cookie.svg +1 -1
- package/icons/material-symbols/copy.svg +1 -1
- package/icons/material-symbols/credit-card.svg +1 -1
- package/icons/material-symbols/currency-circle-dollar.svg +1 -1
- package/icons/material-symbols/cursor-click.svg +1 -1
- package/icons/material-symbols/database.svg +1 -1
- package/icons/material-symbols/desktop.svg +1 -1
- package/icons/material-symbols/device-mobile.svg +1 -1
- package/icons/material-symbols/door-open.svg +1 -1
- package/icons/material-symbols/download-simple.svg +1 -1
- package/icons/material-symbols/envelope-simple.svg +1 -1
- package/icons/material-symbols/eye-slash.svg +1 -1
- package/icons/material-symbols/eye.svg +1 -1
- package/icons/material-symbols/file-pdf.svg +1 -1
- package/icons/material-symbols/file-text.svg +1 -1
- package/icons/material-symbols/file-video.svg +1 -1
- package/icons/material-symbols/file-zip.svg +1 -1
- package/icons/material-symbols/flower-lotus.svg +1 -1
- package/icons/material-symbols/folder-simple.svg +1 -1
- package/icons/material-symbols/funnel.svg +1 -1
- package/icons/material-symbols/gauge.svg +1 -1
- package/icons/material-symbols/gear.svg +1 -1
- package/icons/material-symbols/github-logo.svg +1 -1
- package/icons/material-symbols/globe.svg +1 -1
- package/icons/material-symbols/google-logo.svg +1 -1
- package/icons/material-symbols/graduation-cap.svg +1 -1
- package/icons/material-symbols/hammer.svg +1 -1
- package/icons/material-symbols/hand-heart.svg +1 -1
- package/icons/material-symbols/handshake.svg +1 -1
- package/icons/material-symbols/hard-drives.svg +1 -1
- package/icons/material-symbols/heart.svg +1 -1
- package/icons/material-symbols/house.svg +1 -1
- package/icons/material-symbols/image-square.svg +1 -1
- package/icons/material-symbols/image.svg +1 -1
- package/icons/material-symbols/info.svg +1 -1
- package/icons/material-symbols/keyboard.svg +1 -1
- package/icons/material-symbols/leaf.svg +1 -1
- package/icons/material-symbols/lifebuoy.svg +1 -1
- package/icons/material-symbols/lightbulb.svg +1 -1
- package/icons/material-symbols/link-break.svg +1 -1
- package/icons/material-symbols/link-simple.svg +1 -1
- package/icons/material-symbols/list-bullets.svg +1 -1
- package/icons/material-symbols/list-checks.svg +1 -1
- package/icons/material-symbols/list-numbers.svg +1 -1
- package/icons/material-symbols/list.svg +1 -1
- package/icons/material-symbols/lock-simple-open.svg +1 -1
- package/icons/material-symbols/lock-simple.svg +1 -1
- package/icons/material-symbols/magnifying-glass.svg +1 -1
- package/icons/material-symbols/map-pin.svg +1 -1
- package/icons/material-symbols/map-trifold.svg +1 -1
- package/icons/material-symbols/medal.svg +1 -1
- package/icons/material-symbols/megaphone.svg +1 -1
- package/icons/material-symbols/minus.svg +1 -1
- package/icons/material-symbols/moon.svg +1 -1
- package/icons/material-symbols/newspaper.svg +1 -1
- package/icons/material-symbols/pack.json +29 -29
- package/icons/material-symbols/paint-brush-broad.svg +1 -1
- package/icons/material-symbols/palette.svg +1 -1
- package/icons/material-symbols/paper-plane-tilt.svg +1 -1
- package/icons/material-symbols/pencil-simple.svg +1 -1
- package/icons/material-symbols/phone.svg +1 -1
- package/icons/material-symbols/play-fill.svg +1 -1
- package/icons/material-symbols/plus.svg +1 -1
- package/icons/material-symbols/prohibit.svg +1 -1
- package/icons/material-symbols/puzzle-piece.svg +1 -1
- package/icons/material-symbols/question.svg +1 -1
- package/icons/material-symbols/quotes.svg +1 -1
- package/icons/material-symbols/rows.svg +1 -1
- package/icons/material-symbols/ruler.svg +1 -1
- package/icons/material-symbols/scan.svg +1 -1
- package/icons/material-symbols/shapes.svg +1 -1
- package/icons/material-symbols/share-network.svg +1 -1
- package/icons/material-symbols/shield-check.svg +1 -1
- package/icons/material-symbols/sidebar-simple.svg +1 -1
- package/icons/material-symbols/sign-out.svg +1 -1
- package/icons/material-symbols/signpost.svg +1 -1
- package/icons/material-symbols/sliders-horizontal.svg +1 -1
- package/icons/material-symbols/smiley.svg +1 -1
- package/icons/material-symbols/sparkle.svg +1 -1
- package/icons/material-symbols/square-half-bottom.svg +1 -1
- package/icons/material-symbols/square.svg +1 -1
- package/icons/material-symbols/squares-four.svg +1 -1
- package/icons/material-symbols/sticker.svg +1 -1
- package/icons/material-symbols/storefront.svg +1 -1
- package/icons/material-symbols/sun.svg +1 -1
- package/icons/material-symbols/swatches.svg +1 -1
- package/icons/material-symbols/tag.svg +1 -1
- package/icons/material-symbols/text-aa.svg +1 -1
- package/icons/material-symbols/text-align-left.svg +1 -1
- package/icons/material-symbols/text-b.svg +1 -1
- package/icons/material-symbols/text-h-one.svg +1 -1
- package/icons/material-symbols/text-h-three.svg +1 -1
- package/icons/material-symbols/text-h-two.svg +1 -1
- package/icons/material-symbols/text-italic.svg +1 -1
- package/icons/material-symbols/text-t.svg +1 -1
- package/icons/material-symbols/ticket.svg +1 -1
- package/icons/material-symbols/toggle-right.svg +1 -1
- package/icons/material-symbols/train.svg +1 -1
- package/icons/material-symbols/trash.svg +1 -1
- package/icons/material-symbols/upload-simple.svg +1 -1
- package/icons/material-symbols/user-circle.svg +1 -1
- package/icons/material-symbols/user.svg +1 -1
- package/icons/material-symbols/users-three.svg +1 -1
- package/icons/material-symbols/users.svg +1 -1
- package/icons/material-symbols/video-camera.svg +1 -1
- package/icons/material-symbols/warning-circle.svg +1 -1
- package/icons/material-symbols/warning.svg +1 -1
- package/icons/material-symbols/wave-sine.svg +1 -1
- package/icons/material-symbols/wheelchair.svg +1 -1
- package/icons/material-symbols/wrench.svg +1 -1
- package/icons/material-symbols/x.svg +1 -1
- package/icons/phosphor/pack.json +8 -8
- package/lib/apps.mjs +142 -139
- package/lib/apps.test.d.mts +1 -0
- package/lib/apps.test.mjs +50 -0
- package/lib/blocks.mjs +90 -90
- package/lib/caseCollisions.d.mts +4 -0
- package/lib/caseCollisions.mjs +62 -0
- package/lib/caseCollisions.test.d.mts +1 -0
- package/lib/caseCollisions.test.mjs +74 -0
- package/lib/colors.ts +86 -86
- package/lib/contactBar.ts +18 -18
- package/lib/contracts.d.mts +15 -0
- package/lib/contracts.mjs +17 -0
- package/lib/cookieCategories.mjs +28 -28
- package/lib/crop.mjs +83 -83
- package/lib/csp.mjs +130 -130
- package/lib/cspSources.mjs +60 -60
- package/lib/dynamic-colors.ts +426 -426
- package/lib/git.mjs +64 -64
- package/lib/hooks.ts +51 -51
- package/lib/inline.ts +20 -20
- package/lib/license-texts/Apache-2.0.txt +190 -190
- package/lib/license-texts/BSD-2-Clause.txt +24 -24
- package/lib/license-texts/BSD-3-Clause.txt +28 -28
- package/lib/license-texts/ISC.txt +15 -15
- package/lib/license-texts/MIT.txt +21 -21
- package/lib/licenses.d.mts +1 -1
- package/lib/licenses.mjs +683 -683
- package/lib/markdown.ts +42 -42
- package/lib/motion-schemes.mjs +5 -5
- package/lib/motion.ts +131 -131
- package/lib/notice.mjs +34 -34
- package/lib/scheduled.mjs +37 -37
- package/lib/snackbar.ts +149 -149
- package/lib/status.mjs +18 -18
- package/lib/storeEntries.d.mts +18 -0
- package/lib/storeEntries.mjs +64 -0
- package/lib/storeEntries.test.d.mts +1 -0
- package/lib/storeEntries.test.mjs +71 -0
- package/lib/svg.mjs +231 -231
- package/lib/text-colors.mjs +43 -43
- package/lib/video-id.mjs +72 -72
- package/lib/video.mjs +91 -91
- package/package.json +136 -131
- package/pages/404.astro +10 -10
- package/pages/[...slug].astro +51 -51
- package/pages/_headers.txt +31 -31
- package/pages/blog/index.astro +10 -10
- package/pages/blog/kategori/[category].astro +18 -18
- package/pages/editor/code-images.json.js +25 -25
- package/pages/editor/index.astro +75 -75
- package/pages/editor/manifest.webmanifest.js +23 -23
- package/pages/editor/sw.js +33 -33
- package/pages/editor/sw.js.js +4 -4
- package/pages/editor/thumbnails.json.js +21 -21
- package/pages/editor/version.json.js +22 -22
- package/pages/headers.js +4 -4
- package/pages/index.astro +10 -10
- package/pages/ny.astro +6 -6
- package/pages/robots.txt.js +3 -3
- package/pages/rss.xml.js +20 -20
- package/scripts/app-screenshots.mjs +164 -164
- package/scripts/app-workers.mjs +6 -6
- package/scripts/bin.mjs +12 -12
- package/scripts/built-site.mjs +134 -99
- package/scripts/check-consent.mjs +113 -113
- package/scripts/check-content.mjs +151 -149
- package/scripts/check-demo.mjs +24 -24
- package/scripts/check-store.mjs +160 -0
- package/scripts/check-themes.mjs +45 -45
- package/scripts/check-worker.mjs +569 -569
- package/scripts/compare-html.mjs +81 -81
- package/scripts/declarations.mjs +10 -0
- package/scripts/declarations.test.mjs +32 -0
- package/scripts/demo-site.mjs +42 -42
- package/scripts/editor-icons.mjs +56 -56
- package/scripts/editor-roundtrip.mjs +134 -134
- package/scripts/editor-screens.mjs +101 -101
- package/scripts/git-env.mjs +9 -9
- package/scripts/icon-packs.mjs +151 -151
- package/scripts/link-counts.mjs +22 -22
- package/scripts/page-colors.mjs +42 -42
- package/scripts/releases.mjs +80 -80
- package/scripts/responsive-images.mjs +94 -94
- package/scripts/scheduled-list.mjs +15 -15
- package/scripts/theme-screenshots.mjs +88 -88
- package/scripts/types.mjs +17 -21
- package/scripts/video-thumbnails.mjs +89 -89
- package/scripts/worker-modules.mjs +34 -34
- package/site/appCookies.mjs +74 -74
- package/site/apps.ts +21 -21
- package/site/autolinks.mjs +200 -200
- package/site/blockScripts.ts +31 -31
- package/site/categories.ts +16 -16
- package/site/consent.ts +26 -26
- package/site/content.config.ts +84 -84
- package/site/content.ts +104 -104
- package/site/cookieList.ts +17 -17
- package/site/defineSite.d.mts +2 -14
- package/site/defineSite.mjs +131 -136
- package/site/directives/interact.mjs +31 -31
- package/site/editor-version.mjs +26 -26
- package/site/fill.ts +13 -13
- package/site/format.ts +11 -11
- package/site/home.ts +66 -66
- package/site/iconPacks.mjs +43 -43
- package/site/icons.mjs +605 -605
- package/site/images.mjs +157 -157
- package/site/motion.ts +13 -13
- package/site/noindex.mjs +21 -21
- package/site/pageColors.ts +110 -110
- package/site/popups.ts +98 -98
- package/site/schema.ts +189 -189
- package/site/sections.ts +3 -3
- package/site/seo.ts +28 -28
- package/site/site.ts +76 -76
- package/site/siteConfig.mjs +8 -8
- package/site/siteCsp.mjs +75 -75
- package/site/siteModules.mjs +27 -27
- package/site/standardPages.ts +59 -59
- package/site/storeTheme.d.mts +6 -0
- package/site/storeTheme.mjs +29 -0
- package/site/storeTheme.test.d.mts +1 -0
- package/site/storeTheme.test.mjs +53 -0
- package/site/testimonials.ts +48 -48
- package/site/text.ts +43 -43
- package/site/theme.mjs +483 -472
- package/site/tracking.client.ts +225 -225
- package/site/tracking.ts +9 -9
- package/src/ActionParts.tsx +205 -205
- package/src/Advanced.tsx +152 -152
- package/src/App.tsx +901 -904
- package/src/AppList.tsx +1362 -1345
- package/src/BlockDialog.tsx +360 -360
- package/src/BlockParts.tsx +652 -652
- package/src/BlockSettings.tsx +533 -533
- package/src/BrandForm.tsx +164 -164
- package/src/Colors.tsx +476 -476
- package/src/CommunicationParts.tsx +199 -199
- package/src/CompanyForm.tsx +209 -209
- package/src/Consents.tsx +175 -175
- package/src/ContainmentParts.tsx +185 -185
- package/src/Crop.tsx +152 -152
- package/src/DataForm.tsx +434 -434
- package/src/Deleted.tsx +177 -177
- package/src/EntryForm.tsx +740 -740
- package/src/FileView.tsx +30 -30
- package/src/FormFrame.tsx +237 -237
- package/src/Keywords.tsx +225 -225
- package/src/Library.tsx +1563 -1563
- package/src/Licenses.tsx +336 -336
- package/src/LinkSheet.tsx +138 -138
- package/src/MarkdownEditor.tsx +1075 -1075
- package/src/Media.tsx +677 -677
- package/src/NavigationParts.tsx +350 -350
- package/src/NavigationRestParts.tsx +247 -247
- package/src/Notifications.tsx +59 -59
- package/src/PagePicker.tsx +136 -136
- package/src/Parts.tsx +991 -991
- package/src/PartsShown.tsx +168 -168
- package/src/PickerParts.tsx +183 -183
- package/src/Redirects.tsx +450 -450
- package/src/Screen.tsx +184 -184
- package/src/SectionsField.tsx +65 -65
- package/src/SelectionParts.tsx +134 -134
- package/src/SeoScores.tsx +184 -184
- package/src/Sheet.tsx +35 -35
- package/src/ThemeStore.tsx +376 -376
- package/src/Tokens.tsx +346 -346
- package/src/Versions.tsx +214 -214
- package/src/addresses.ts +75 -75
- package/src/allIcons.ts +7 -7
- package/src/appFiles.ts +29 -29
- package/src/apps.ts +281 -281
- package/src/ask.tsx +123 -123
- package/src/blocks.ts +251 -251
- package/src/brand.ts +153 -153
- package/src/cache.ts +42 -42
- package/src/check.ts +285 -285
- package/src/checkSite.ts +256 -256
- package/src/{consents.ts → consentData.ts} +26 -26
- package/src/cookieList.ts +70 -70
- package/src/countries.ts +59 -59
- package/src/deploy.ts +155 -155
- package/src/editor.css +602 -602
- package/src/examples.ts +85 -85
- package/src/fields.tsx +942 -942
- package/src/frontmatter.mjs +42 -42
- package/src/github.ts +376 -376
- package/src/i18n/da-DK/advanced.ts +28 -28
- package/src/i18n/da-DK/app.ts +71 -71
- package/src/i18n/da-DK/apps.ts +90 -90
- package/src/i18n/da-DK/blockSettings.ts +38 -38
- package/src/i18n/da-DK/blocks.ts +408 -408
- package/src/i18n/da-DK/check.ts +25 -25
- package/src/i18n/da-DK/colors.ts +64 -64
- package/src/i18n/da-DK/common.ts +118 -118
- package/src/i18n/da-DK/company.ts +24 -24
- package/src/i18n/da-DK/consents.ts +23 -23
- package/src/i18n/da-DK/forms.ts +116 -116
- package/src/i18n/da-DK/history.ts +35 -35
- package/src/i18n/da-DK/index.ts +49 -49
- package/src/i18n/da-DK/keywords.ts +19 -19
- package/src/i18n/da-DK/library.ts +135 -135
- package/src/i18n/da-DK/licenses.ts +25 -25
- package/src/i18n/da-DK/media.ts +79 -79
- package/src/i18n/da-DK/parts.ts +696 -696
- package/src/i18n/da-DK/preview.ts +13 -13
- package/src/i18n/da-DK/redirects.ts +51 -51
- package/src/i18n/da-DK/seo.ts +20 -20
- package/src/i18n/da-DK/text.ts +35 -35
- package/src/i18n/da-DK/theme.ts +41 -41
- package/src/i18n/en-US/advanced.ts +32 -32
- package/src/i18n/en-US/app.ts +84 -84
- package/src/i18n/en-US/apps.ts +134 -134
- package/src/i18n/en-US/blockSettings.ts +48 -48
- package/src/i18n/en-US/blocks.ts +441 -441
- package/src/i18n/en-US/check.ts +31 -31
- package/src/i18n/en-US/colors.ts +82 -82
- package/src/i18n/en-US/common.ts +144 -144
- package/src/i18n/en-US/company.ts +25 -25
- package/src/i18n/en-US/consents.ts +25 -25
- package/src/i18n/en-US/forms.ts +145 -145
- package/src/i18n/en-US/history.ts +43 -43
- package/src/i18n/en-US/index.ts +51 -51
- package/src/i18n/en-US/keywords.ts +24 -24
- package/src/i18n/en-US/library.ts +176 -176
- package/src/i18n/en-US/licenses.ts +36 -36
- package/src/i18n/en-US/media.ts +80 -80
- package/src/i18n/en-US/parts.ts +718 -718
- package/src/i18n/en-US/preview.ts +15 -15
- package/src/i18n/en-US/redirects.ts +66 -66
- package/src/i18n/en-US/seo.ts +26 -26
- package/src/i18n/en-US/text.ts +41 -41
- package/src/i18n/en-US/theme.ts +41 -41
- package/src/i18n/index.ts +124 -124
- package/src/icon.tsx +160 -160
- package/src/iconPacks.ts +89 -89
- package/src/images.ts +163 -163
- package/src/{licenses.ts → licenseData.ts} +79 -79
- package/src/markdown.mjs +204 -204
- package/src/nav.ts +68 -68
- package/src/notFound.ts +28 -28
- package/src/{notifications.ts → notificationData.ts} +223 -223
- package/src/pagePreview.tsx +204 -204
- package/src/r2Caching.ts +55 -55
- package/src/{redirects.ts → redirectData.ts} +149 -149
- package/src/registry.tsx +188 -188
- package/src/schema.ts +417 -417
- package/src/sections.mjs +303 -303
- package/src/seo.ts +333 -333
- package/src/shared.ts +44 -44
- package/src/sidebar.ts +31 -31
- package/src/site.ts +317 -317
- package/src/standard/Block.astro +19 -19
- package/src/standard/BlockSite.astro +18 -18
- package/src/standard/accordion/index.tsx +47 -47
- package/src/standard/accordion/load.ts +23 -23
- package/src/standard/accordion/shared.ts +15 -15
- package/src/standard/accordion/view.ts +32 -32
- package/src/standard/buttons/fields.ts +98 -98
- package/src/standard/buttons/index.tsx +23 -23
- package/src/standard/buttons/load.ts +34 -34
- package/src/standard/buttons/shared.ts +12 -12
- package/src/standard/buttons/view.ts +30 -30
- package/src/standard/cards/index.tsx +71 -71
- package/src/standard/cards/load.ts +17 -17
- package/src/standard/cards/shared.ts +27 -27
- package/src/standard/cards/view.ts +29 -29
- package/src/standard/checklist/index.tsx +27 -27
- package/src/standard/checklist/load.ts +5 -5
- package/src/standard/checklist/view.ts +13 -13
- package/src/standard/cookies/index.tsx +17 -17
- package/src/standard/cookies/list.ts +56 -56
- package/src/standard/cookies/load.ts +11 -11
- package/src/standard/cookies/shared.ts +20 -20
- package/src/standard/cookies/view.ts +28 -28
- package/src/standard/faq/index.tsx +29 -29
- package/src/standard/faq/load.ts +14 -14
- package/src/standard/faq/view.ts +21 -21
- package/src/standard/head.ts +23 -23
- package/src/standard/hero/fields.ts +48 -48
- package/src/standard/hero/index.tsx +57 -57
- package/src/standard/hero/load.ts +23 -23
- package/src/standard/hero/view.ts +38 -38
- package/src/standard/links/index.tsx +41 -41
- package/src/standard/links/load.ts +17 -17
- package/src/standard/links/view.ts +25 -25
- package/src/standard/list/index.tsx +50 -50
- package/src/standard/list/load.ts +5 -5
- package/src/standard/list/view.ts +18 -18
- package/src/standard/load.ts +7 -7
- package/src/standard/notice/index.tsx +30 -30
- package/src/standard/notice/load.ts +16 -16
- package/src/standard/notice/view.ts +10 -10
- package/src/standard/points/index.tsx +42 -42
- package/src/standard/points/load.ts +5 -5
- package/src/standard/points/view.ts +12 -12
- package/src/standard/posts/index.tsx +132 -132
- package/src/standard/posts/load.ts +36 -36
- package/src/standard/posts/shared.ts +21 -21
- package/src/standard/posts/view.ts +63 -63
- package/src/standard/pricecards/index.tsx +57 -57
- package/src/standard/pricecards/load.ts +11 -11
- package/src/standard/pricecards/view.ts +50 -50
- package/src/standard/sharedTexts.ts +6 -6
- package/src/standard/site.ts +124 -124
- package/src/standard/siteLooks.ts +19 -19
- package/src/standard/sites.ts +13 -13
- package/src/standard/steps/index.tsx +28 -28
- package/src/standard/steps/load.ts +5 -5
- package/src/standard/steps/view.ts +11 -11
- package/src/standard/testimonials/index.tsx +87 -87
- package/src/standard/testimonials/list.ts +43 -43
- package/src/standard/testimonials/load.ts +58 -58
- package/src/standard/testimonials/shared.ts +13 -13
- package/src/standard/testimonials/view.ts +76 -76
- package/src/standard/themeBlocks.ts +13 -13
- package/src/standard/video/index.tsx +23 -23
- package/src/standard/video/load.ts +10 -10
- package/src/standard/video/view.ts +42 -42
- package/src/standard/view.tsx +105 -105
- package/src/standard/views.ts +37 -37
- package/src/store.ts +479 -499
- package/src/theme.ts +90 -90
- package/src/themeUi.ts +97 -97
- package/src/themes.ts +91 -91
- package/src/ui.tsx +3402 -3402
- package/src/virtual.d.ts +5 -5
- package/src/when.ts +22 -22
- package/starter/.githooks/pre-push +4 -4
- package/starter/.nvmrc +1 -1
- package/starter/.prettierignore +11 -11
- package/starter/.prettierrc.json +7 -7
- package/starter/README.md +27 -26
- package/starter/astro.config.mjs +5 -5
- package/starter/package.json +67 -67
- package/starter/public/_redirects +1 -1
- package/starter/public/favicon.svg +1 -1
- package/starter/public/logo.svg +1 -1
- package/starter/src/content/pages/blog.md +8 -8
- package/starter/src/content/pages/forside.md +9 -9
- package/starter/src/content/pages/kontakt.md +7 -7
- package/starter/src/content/pages/privatlivspolitik.md +9 -9
- package/starter/src/content/pages/siden-findes-ikke.md +7 -7
- package/starter/src/content.config.ts +2 -2
- package/starter/src/data/apps.json +1 -1
- package/starter/src/data/block-settings.json +3 -3
- package/starter/src/data/blocks.json +1 -1
- package/starter/src/data/booking.json +6 -6
- package/starter/src/data/colors.json +70 -70
- package/starter/src/data/contact.json +8 -8
- package/starter/src/data/cookies.json +21 -21
- package/starter/src/data/csp.json +1 -1
- package/starter/src/data/footer.json +15 -15
- package/starter/src/data/internal-links.json +4 -4
- package/starter/src/data/menu.json +4 -4
- package/starter/src/data/mobile-menu.json +4 -4
- package/starter/src/data/old-addresses.json +1 -1
- package/starter/src/data/redirects.json +1 -1
- package/starter/src/data/standard-pages.json +8 -8
- package/starter/src/data/theme.json +1 -1
- package/starter/src/env.d.ts +2 -2
- package/starter/src/site/config.ts +13 -13
- package/starter/src/site/worker.js +14 -14
- package/starter/tsconfig.json +17 -17
- package/starter/worker/apps.js +6 -6
- package/starter/wrangler.jsonc +39 -39
- package/theme/LICENSE +21 -21
- package/theme/blocks/ArrowLink.tsx +14 -14
- package/theme/blocks/BlockHead.tsx +26 -26
- package/theme/blocks/Panel.tsx +32 -32
- package/theme/blocks/Person.tsx +26 -26
- package/theme/blocks/accordion/PageRow.tsx +25 -25
- package/theme/blocks/accordion/View.tsx +17 -17
- package/theme/blocks/buttons/Buttons.tsx +28 -28
- package/theme/blocks/buttons/View.tsx +12 -12
- package/theme/blocks/cards/LinkCards.tsx +72 -72
- package/theme/blocks/cards/View.tsx +20 -20
- package/theme/blocks/checklist/Checks.tsx +15 -15
- package/theme/blocks/checklist/View.tsx +14 -14
- package/theme/blocks/cookies/View.tsx +36 -36
- package/theme/blocks/faq/Accordion.tsx +34 -34
- package/theme/blocks/faq/View.tsx +16 -16
- package/theme/blocks/faq/script.ts +56 -56
- package/theme/blocks/hero/View.tsx +44 -44
- package/theme/blocks/links/LinkGroups.tsx +24 -24
- package/theme/blocks/links/View.tsx +17 -17
- package/theme/blocks/list/Rows.tsx +24 -24
- package/theme/blocks/list/View.tsx +14 -14
- package/theme/blocks/notice/View.tsx +13 -13
- package/theme/blocks/points/PointList.tsx +34 -34
- package/theme/blocks/points/View.tsx +14 -14
- package/theme/blocks/posts/View.tsx +26 -26
- package/theme/blocks/pricecards/View.tsx +40 -40
- package/theme/blocks/steps/StepList.tsx +23 -23
- package/theme/blocks/steps/View.tsx +14 -14
- package/theme/blocks/testimonials/List.tsx +46 -46
- package/theme/blocks/testimonials/Slider.tsx +44 -44
- package/theme/blocks/testimonials/Testimonial.tsx +124 -124
- package/theme/blocks/testimonials/View.tsx +21 -21
- package/theme/blocks/testimonials/script.ts +52 -52
- package/theme/blocks/video/View.tsx +41 -41
- package/theme/components/ActionBar.astro +63 -63
- package/theme/components/AppSlot.astro +21 -21
- package/theme/components/ArrowLink.tsx +21 -21
- package/theme/components/BlogIndex.astro +48 -48
- package/theme/components/BlogPosts.astro +18 -18
- package/theme/components/BlogPosts.tsx +45 -45
- package/theme/components/BookButton.tsx +22 -22
- package/theme/components/Container.astro +11 -11
- package/theme/components/CookieConsent.astro +121 -121
- package/theme/components/EndBlock.astro +21 -21
- package/theme/components/FontPreload.astro +6 -6
- package/theme/components/Footer.astro +85 -85
- package/theme/components/Header.astro +186 -186
- package/theme/components/Icon.astro +16 -16
- package/theme/components/Img.astro +29 -29
- package/theme/components/Img.tsx +33 -33
- package/theme/components/Inline.astro +11 -11
- package/theme/components/Logo.astro +23 -23
- package/theme/components/LogoMark.astro +10 -10
- package/theme/components/Markdown.astro +17 -17
- package/theme/components/MobileMenu.tsx +224 -224
- package/theme/components/MobileNav.astro +33 -33
- package/theme/components/PageHeader.astro +22 -22
- package/theme/components/PageSection.astro +24 -24
- package/theme/components/PhoneButton.tsx +11 -11
- package/theme/components/Popup.astro +145 -145
- package/theme/components/PopupButton.tsx +24 -24
- package/theme/components/PopupView.tsx +48 -48
- package/theme/components/PostList.tsx +58 -58
- package/theme/components/PostMeta.tsx +17 -17
- package/theme/components/RichText.astro +28 -28
- package/theme/components/Section.astro +29 -29
- package/theme/components/SectionHeading.tsx +8 -8
- package/theme/components/TextSection.astro +16 -16
- package/theme/components/home/PageBlock.astro +19 -19
- package/theme/components/home/Text.astro +13 -13
- package/theme/components/loadPopup.ts +22 -22
- package/theme/components/loadPosts.ts +16 -16
- package/theme/components/posts.ts +15 -15
- package/theme/editor/looks.mjs +7 -7
- package/theme/editor/previews.tsx +56 -56
- package/theme/layouts/Base.astro +273 -273
- package/theme/layouts/PageLayout.astro +126 -126
- package/theme/layouts/PostLayout.astro +137 -137
- package/theme/scripts/motion.ts +29 -29
- package/theme/styles/base.css +320 -320
- package/theme/styles/blocks.css +1808 -1808
- package/theme/styles/components.css +206 -206
- package/theme/styles/global.css +1302 -1302
- package/theme/styles/ui.css +6544 -6544
- package/theme/templates/Blog.astro +38 -38
- package/theme/templates/Category.astro +19 -19
- package/theme/templates/Home.astro +36 -36
- package/theme/templates/New.astro +68 -68
- package/theme/templates/NotFound.astro +117 -117
- package/theme/theme.json +28 -28
- package/theme/ui/Badge.tsx +45 -45
- package/theme/ui/BottomSheet.tsx +148 -148
- package/theme/ui/Button.tsx +79 -79
- package/theme/ui/ButtonGroup.tsx +31 -31
- package/theme/ui/Card.tsx +45 -45
- package/theme/ui/Carousel.tsx +124 -124
- package/theme/ui/Checkbox.tsx +59 -59
- package/theme/ui/Chip.tsx +101 -101
- package/theme/ui/ChipSet.tsx +27 -27
- package/theme/ui/DatePicker.tsx +149 -149
- package/theme/ui/DatePickerContainer.tsx +316 -316
- package/theme/ui/Dialog.tsx +100 -100
- package/theme/ui/Divider.tsx +27 -27
- package/theme/ui/ExposedDropdownMenu.tsx +77 -77
- package/theme/ui/Fab.tsx +88 -88
- package/theme/ui/FabMenu.tsx +67 -67
- package/theme/ui/IconButton.tsx +78 -78
- package/theme/ui/List.tsx +287 -287
- package/theme/ui/LoadingIndicator.tsx +29 -29
- package/theme/ui/Menu.tsx +278 -278
- package/theme/ui/NavigationBar.tsx +62 -62
- package/theme/ui/NavigationDrawer.tsx +80 -80
- package/theme/ui/NavigationRail.tsx +154 -154
- package/theme/ui/ProgressIndicator.tsx +162 -162
- package/theme/ui/RadioButton.tsx +50 -50
- package/theme/ui/Search.tsx +129 -129
- package/theme/ui/SegmentedButton.tsx +49 -49
- package/theme/ui/SideSheet.tsx +54 -54
- package/theme/ui/Slider.tsx +155 -155
- package/theme/ui/Snackbar.tsx +67 -67
- package/theme/ui/SplitButton.tsx +67 -67
- package/theme/ui/Switch.tsx +60 -60
- package/theme/ui/Tabs.tsx +114 -114
- package/theme/ui/TextField.tsx +278 -278
- package/theme/ui/TimePicker.tsx +128 -128
- package/theme/ui/TimePickerContainer.tsx +234 -234
- package/theme/ui/Toolbar.tsx +67 -67
- package/theme/ui/Tooltip.tsx +58 -58
- package/theme/ui/TopAppBar.tsx +77 -77
- package/theme/ui/bottomSheet.script.ts +8 -8
- package/theme/ui/{bottomSheet.ts → bottomSheetControl.ts} +211 -211
- package/theme/ui/carousel.script.ts +28 -28
- package/theme/ui/carouselLayout.ts +229 -229
- package/theme/ui/checkbox.script.ts +8 -8
- package/theme/ui/datePicker.script.ts +302 -302
- package/theme/ui/dates.ts +185 -185
- package/theme/ui/{dialog.ts → dialogControl.ts} +48 -48
- package/theme/ui/dropdownMenu.script.ts +166 -166
- package/theme/ui/fabMenu.script.ts +57 -57
- package/theme/ui/navigationRail.script.ts +47 -47
- package/theme/ui/ripple.script.ts +159 -159
- package/theme/ui/search.script.ts +92 -92
- package/theme/ui/{sideSheet.ts → sideSheetControl.ts} +48 -48
- package/theme/ui/slider.script.ts +12 -12
- package/theme/ui/sliderUpdate.ts +52 -52
- package/theme/ui/snackbar.script.ts +16 -16
- package/theme/ui/splitButton.script.ts +63 -63
- package/theme/ui/tabs.script.ts +48 -48
- package/theme/ui/textField.script.ts +17 -17
- package/theme/ui/textFieldUpdate.ts +44 -44
- package/theme/ui/timePicker.script.ts +248 -248
- package/theme/ui/toolbar.script.ts +43 -43
- package/theme/ui/tooltip.script.ts +68 -68
- package/worker/consent.js +141 -141
- package/worker/editor-github.js +167 -167
- package/worker/editor-host.js +74 -74
- package/worker/files.js +401 -401
- package/worker/github.js +36 -36
- package/worker/index.js +188 -184
- package/worker/not-found.js +97 -97
- package/worker/oauth.js +95 -95
- package/worker/publish.js +52 -52
- package/worker/push.js +270 -270
- package/worker/r2-status.js +50 -50
- package/worker/session.js +58 -58
- package/worker/store.js +46 -0
package/worker/push.js
CHANGED
|
@@ -1,270 +1,270 @@
|
|
|
1
|
-
// The editor's notifications on the phone or computer also when the editor is closed: Web Push from the Worker
|
|
2
|
-
// (packages/editor/src/
|
|
3
|
-
// "Notifikationer" signs up here with the browser's push subscription, the types it has turned off and the editor's
|
|
4
|
-
// language; the Worker sends to the browser's push service (Google's, Mozilla's, Apple's), which wakes the editor's
|
|
5
|
-
// service worker, which shows it. Kept in Workers KV (the binding PUSH in wrangler.jsonc):
|
|
6
|
-
// vapid the Worker's own key pair for the push services (VAPID, RFC 8292), made the first time it is needed;
|
|
7
|
-
// a device signed up with another key signs up again when the editor opens
|
|
8
|
-
// device:<id> a device: its subscription, the types turned off and the language
|
|
9
|
-
// wait:<id> the device's last save, until the site is built with it: the commit and the two texts to send
|
|
10
|
-
// waiting until when there may be a wait (so the cron each minute asks KV once, and lists only then)
|
|
11
|
-
// What is sent:
|
|
12
|
-
// - "Hjemmesiden er opdateret med dine ændringer" (type `site`) to the device that saved, when the site has been built
|
|
13
|
-
// with the save (each minute, waitForBuilds: the cron in wrangler.jsonc), or that it was not, after ten minutes;
|
|
14
|
-
// the texts are the editor's, in its language. An editor on screen says it itself and takes the wait back.
|
|
15
|
-
// - an app's notification from its part of the Worker (`notify` in the tools it is given, packages/editor/worker/index.js) to each
|
|
16
|
-
// device with the type on, in the device's language (the app's texts.ts).
|
|
17
|
-
// The payload is encrypted for the device (RFC 8291, aes128gcm); a device the push service no longer knows is removed.
|
|
18
|
-
// GET /api/push { key }: the Worker's public key, for the browser's subscription
|
|
19
|
-
// PUT /api/push { subscription, off, locale }: signs a device up, or updates its choices
|
|
20
|
-
// DELETE /api/push { endpoint }: signs it off ("På telefonen" off, "Log ud")
|
|
21
|
-
// PUT /api/push/wait { endpoint, commit, text, failText, to? }: sends `text` when the site is built with `commit`
|
|
22
|
-
// DELETE /api/push/wait { endpoint }: takes it back (the editor said it itself)
|
|
23
|
-
// Only someone who can save the site (write access to the repository on GitHub) can use them. Without the binding (a
|
|
24
|
-
// preview of a branch) nothing is kept or sent, and the editor's notifications come only while it is open.
|
|
25
|
-
import { config } from '@site/config';
|
|
26
|
-
import { contact } from '@site/worker';
|
|
27
|
-
import { json } from './not-found.js';
|
|
28
|
-
import { mayEdit } from './session.js';
|
|
29
|
-
|
|
30
|
-
const VAPID = 'vapid';
|
|
31
|
-
const DEVICE = 'device:';
|
|
32
|
-
const WAIT = 'wait:';
|
|
33
|
-
const WAITING = 'waiting';
|
|
34
|
-
// How long the editor waits for a save to be on the site (packages/editor/src/deploy.ts GIVE_UP).
|
|
35
|
-
const GIVE_UP = 10 * 60_000;
|
|
36
|
-
// A device not seen (the editor not opened on it) for half a year drops out by itself.
|
|
37
|
-
const KEEP = 180 * 24 * 60 * 60;
|
|
38
|
-
// How long a push service keeps a notification for a device that is off.
|
|
39
|
-
const TTL = 24 * 60 * 60;
|
|
40
|
-
|
|
41
|
-
const encoder = new TextEncoder();
|
|
42
|
-
|
|
43
|
-
function b64url(bytes) {
|
|
44
|
-
let binary = '';
|
|
45
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
46
|
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function fromB64url(text) {
|
|
50
|
-
const binary = atob(text.replace(/-/g, '+').replace(/_/g, '/'));
|
|
51
|
-
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const concat = (...parts) => {
|
|
55
|
-
const all = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
56
|
-
let at = 0;
|
|
57
|
-
for (const p of parts) all.set(p, (at += p.length) - p.length);
|
|
58
|
-
return all;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/** A device's id: a hash of its endpoint, so the endpoint is not a key. */
|
|
62
|
-
async function idOf(endpoint) {
|
|
63
|
-
const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(endpoint)));
|
|
64
|
-
return b64url(hash.slice(0, 16));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The Worker's key pair as a JWK with the private part, made and kept the first time. */
|
|
68
|
-
async function vapidOf(kv) {
|
|
69
|
-
const kept = await kv.get(VAPID, 'json');
|
|
70
|
-
if (kept) return kept;
|
|
71
|
-
const pair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']);
|
|
72
|
-
const jwk = await crypto.subtle.exportKey('jwk', pair.privateKey);
|
|
73
|
-
await kv.put(VAPID, JSON.stringify(jwk));
|
|
74
|
-
return jwk;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** The public key as the browser takes it (an uncompressed point, base64url). */
|
|
78
|
-
const publicKeyOf = (jwk) => b64url(concat(new Uint8Array([4]), fromB64url(jwk.x), fromB64url(jwk.y)));
|
|
79
|
-
|
|
80
|
-
/** The header that says the push comes from this site (VAPID, RFC 8292). */
|
|
81
|
-
async function vapidHeader(jwk, endpoint) {
|
|
82
|
-
const key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
|
|
83
|
-
const part = (o) => b64url(encoder.encode(JSON.stringify(o)));
|
|
84
|
-
const claims = { aud: new URL(endpoint).origin, exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60, sub: `https://${config.domain}` };
|
|
85
|
-
const unsigned = `${part({ typ: 'JWT', alg: 'ES256' })}.${part(claims)}`;
|
|
86
|
-
const signature = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, key, encoder.encode(unsigned));
|
|
87
|
-
return `vapid t=${unsigned}.${b64url(new Uint8Array(signature))}, k=${publicKeyOf(jwk)}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function hkdf(salt, ikm, info, bytes) {
|
|
91
|
-
const key = await crypto.subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
|
|
92
|
-
return new Uint8Array(await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info }, key, bytes * 8));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** The payload encrypted for the device (RFC 8291, the content coding aes128gcm of RFC 8188, one record).
|
|
96
|
-
* `salt` and `pair` are for the tests only. */
|
|
97
|
-
export async function encrypt(payload, { p256dh, auth }, { salt = crypto.getRandomValues(new Uint8Array(16)), pair } = {}) {
|
|
98
|
-
const device = fromB64url(p256dh);
|
|
99
|
-
const own = pair ?? (await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']));
|
|
100
|
-
const ownPublic = new Uint8Array(await crypto.subtle.exportKey('raw', own.publicKey));
|
|
101
|
-
const deviceKey = await crypto.subtle.importKey('raw', device, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
|
|
102
|
-
const shared = new Uint8Array(await crypto.subtle.deriveBits({ name: 'ECDH', public: deviceKey }, own.privateKey, 256));
|
|
103
|
-
const ikm = await hkdf(fromB64url(auth), shared, concat(encoder.encode('WebPush: info\0'), device, ownPublic), 32);
|
|
104
|
-
const cek = await hkdf(salt, ikm, encoder.encode('Content-Encoding: aes128gcm\0'), 16);
|
|
105
|
-
const nonce = await hkdf(salt, ikm, encoder.encode('Content-Encoding: nonce\0'), 12);
|
|
106
|
-
const key = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']);
|
|
107
|
-
// The last (and only) record ends with 2.
|
|
108
|
-
const plain = concat(encoder.encode(payload), new Uint8Array([2]));
|
|
109
|
-
const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, plain));
|
|
110
|
-
const size = new Uint8Array(4);
|
|
111
|
-
new DataView(size.buffer).setUint32(0, 4096);
|
|
112
|
-
return concat(salt, size, new Uint8Array([ownPublic.length]), ownPublic, sealed);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Sends a notification ({ title, body, to, tag, error }) to a device; removes the device when its push service no
|
|
116
|
-
* longer knows it. */
|
|
117
|
-
async function send(env, id, device, message) {
|
|
118
|
-
const { endpoint, keys } = device.subscription;
|
|
119
|
-
try {
|
|
120
|
-
const jwk = await vapidOf(env.PUSH);
|
|
121
|
-
const res = await fetch(endpoint, {
|
|
122
|
-
method: 'POST',
|
|
123
|
-
headers: {
|
|
124
|
-
authorization: await vapidHeader(jwk, endpoint),
|
|
125
|
-
'content-encoding': 'aes128gcm',
|
|
126
|
-
'content-type': 'application/octet-stream',
|
|
127
|
-
ttl: String(TTL),
|
|
128
|
-
urgency: 'normal',
|
|
129
|
-
},
|
|
130
|
-
body: await encrypt(JSON.stringify({ title: contact.name, ...message }), keys),
|
|
131
|
-
});
|
|
132
|
-
if (res.status === 404 || res.status === 410) await env.PUSH.delete(DEVICE + id);
|
|
133
|
-
else if (!res.ok) console.error(`Notifikationen kunne ikke sendes: ${res.status} ${(await res.text()).slice(0, 200)}`);
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.error(`Notifikationen kunne ikke sendes: ${e?.message ?? e}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** The devices signed up, by their ids. */
|
|
140
|
-
async function devices(kv) {
|
|
141
|
-
const all = [];
|
|
142
|
-
let cursor;
|
|
143
|
-
for (let i = 0; i < 5; i++) {
|
|
144
|
-
const page = await kv.list({ prefix: DEVICE, cursor });
|
|
145
|
-
all.push(...page.keys.map((k) => k.name.slice(DEVICE.length)));
|
|
146
|
-
if (page.list_complete) break;
|
|
147
|
-
cursor = page.cursor;
|
|
148
|
-
}
|
|
149
|
-
const kept = await Promise.all(all.map((id) => kv.get(DEVICE + id, 'json')));
|
|
150
|
-
return all.map((id, i) => ({ id, device: kept[i] })).filter((d) => d.device?.subscription);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Sends an app's notification ({ type, id, text, error, to }, its type as `<app>/<type>`) to each device with the type
|
|
154
|
-
* on, its text in the device's language (`texts`: the app's texts.ts). */
|
|
155
|
-
export async function notifyAll(env, { type, id, text, error, to }, texts = {}) {
|
|
156
|
-
if (!env.PUSH) return;
|
|
157
|
-
for (const { id: device, device: d } of await devices(env.PUSH)) {
|
|
158
|
-
if (d.off?.includes(type)) continue;
|
|
159
|
-
await send(env, device, d, { body: texts[d.locale]?.[text] ?? text, error: !!error, to, tag: `${type}:${id}` });
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** What the site was last built from (/editor/version.json, packages/editor/pages/editor/version.json.js). */
|
|
164
|
-
async function builtOf(env) {
|
|
165
|
-
try {
|
|
166
|
-
const res = await env.ASSETS.fetch(new Request('https://assets.local/editor/version.json'));
|
|
167
|
-
return res.ok ? await res.json() : {};
|
|
168
|
-
} catch {
|
|
169
|
-
return {};
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Whether the build's commit has `commit` in it (it may be built from a later one): asked of GitHub with the Worker's
|
|
174
|
-
* own token (GITHUB_PUBLISH_TOKEN); without it, only the commit itself counts. */
|
|
175
|
-
async function includes(env, build, commit) {
|
|
176
|
-
if (build === commit) return true;
|
|
177
|
-
if (!env.GITHUB_PUBLISH_TOKEN) return false;
|
|
178
|
-
const { owner, name } = config.repo;
|
|
179
|
-
const res = await fetch(`https://api.github.com/repos/${owner}/${name}/compare/${commit}...${build}`, {
|
|
180
|
-
headers: { authorization: `Bearer ${env.GITHUB_PUBLISH_TOKEN}`, accept: 'application/vnd.github+json', 'user-agent': `${name}-worker` },
|
|
181
|
-
});
|
|
182
|
-
if (!res.ok) return false;
|
|
183
|
-
const { status } = await res.json().catch(() => ({}));
|
|
184
|
-
return status === 'ahead' || status === 'identical';
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Each minute: tells each device waiting for its save whether the site has been built with it (see above). */
|
|
188
|
-
export async function waitForBuilds(env, now = Date.now()) {
|
|
189
|
-
if (!env.PUSH) return;
|
|
190
|
-
const until = Number(await env.PUSH.get(WAITING));
|
|
191
|
-
if (!until || now > until) return;
|
|
192
|
-
const { keys } = await env.PUSH.list({ prefix: WAIT });
|
|
193
|
-
if (!keys.length) return;
|
|
194
|
-
const built = await builtOf(env);
|
|
195
|
-
const builtAt = Date.parse(built.builtAt ?? '');
|
|
196
|
-
for (const { name, metadata: wait } of keys) {
|
|
197
|
-
if (!wait) continue;
|
|
198
|
-
const at = Date.parse(wait.at);
|
|
199
|
-
// A build made before the save cannot have it.
|
|
200
|
-
const online = !!built.commit && builtAt >= at && (await includes(env, built.commit, wait.commit).catch(() => false));
|
|
201
|
-
const late = now - at > GIVE_UP;
|
|
202
|
-
if (!online && !late) continue;
|
|
203
|
-
await env.PUSH.delete(name);
|
|
204
|
-
const device = await env.PUSH.get(DEVICE + wait.device, 'json');
|
|
205
|
-
// Without any version file (a build from before it existed) there is nothing to tell, as in the editor.
|
|
206
|
-
if (!device?.subscription || device.off?.includes('site') || (!online && !built.commit)) continue;
|
|
207
|
-
await send(env, wait.device, device, { body: online ? wait.text : wait.failText, error: !online, to: wait.to, tag: 'site' });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const text = (value, max = 300) => (typeof value === 'string' ? value.slice(0, max) : '');
|
|
212
|
-
const isRoute = (to) => typeof to === 'string' && /^[\w\-/æøåÆØÅ]{1,200}$/.test(to);
|
|
213
|
-
|
|
214
|
-
// The browsers' push services (Chrome's, Firefox's, Safari's, Edge's): the Worker sends to no other address.
|
|
215
|
-
const PUSH_SERVICE =
|
|
216
|
-
/^https:\/\/(fcm\.googleapis\.com|android\.googleapis\.com|updates\.push\.services\.mozilla\.com|[\w-]+(\.[\w-]+)*\.push\.apple\.com|[\w-]+(\.[\w-]+)*\.notify\.windows\.com)\//;
|
|
217
|
-
|
|
218
|
-
function subscriptionOf(value) {
|
|
219
|
-
const { endpoint, keys } = value ?? {};
|
|
220
|
-
if (typeof endpoint !== 'string' || !PUSH_SERVICE.test(endpoint) || endpoint.length > 1000) return null;
|
|
221
|
-
if (typeof keys?.p256dh !== 'string' || typeof keys?.auth !== 'string') return null;
|
|
222
|
-
return { endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** The addresses /api/push and /api/push/wait (see above). */
|
|
226
|
-
export async function handlePush(request, env) {
|
|
227
|
-
if (!(await mayEdit(request))) return json({ error: 'forbidden' }, 403);
|
|
228
|
-
if (!env.PUSH) return json({ error: 'unavailable' }, 503);
|
|
229
|
-
const wait = new URL(request.url).pathname.replace(/\/$/, '') === '/api/push/wait';
|
|
230
|
-
if (request.method === 'GET' && !wait) return json({ key: publicKeyOf(await vapidOf(env.PUSH)) });
|
|
231
|
-
const body = await request.json().catch(() => ({}));
|
|
232
|
-
const endpoint = body.endpoint ?? body.subscription?.endpoint;
|
|
233
|
-
if (typeof endpoint !== 'string' || !endpoint) return json({ error: 'endpoint' }, 400);
|
|
234
|
-
const id = await idOf(endpoint);
|
|
235
|
-
|
|
236
|
-
if (wait && request.method === 'PUT') {
|
|
237
|
-
if (typeof body.commit !== 'string' || !/^[0-9a-f]{40}$/.test(body.commit)) return json({ error: 'commit' }, 400);
|
|
238
|
-
const at = new Date().toISOString();
|
|
239
|
-
const metadata = { commit: body.commit, device: id, text: text(body.text), failText: text(body.failText), at };
|
|
240
|
-
if (isRoute(body.to)) metadata.to = body.to;
|
|
241
|
-
// The value is empty: all of it is in the metadata, which the list each minute gives with the key.
|
|
242
|
-
await env.PUSH.put(WAIT + id, '', { metadata, expirationTtl: (2 * GIVE_UP) / 1000 });
|
|
243
|
-
await env.PUSH.put(WAITING, String(Date.now() + GIVE_UP + 2 * 60_000));
|
|
244
|
-
return json({ ok: true });
|
|
245
|
-
}
|
|
246
|
-
if (wait && request.method === 'DELETE') {
|
|
247
|
-
await env.PUSH.delete(WAIT + id);
|
|
248
|
-
return json({ ok: true });
|
|
249
|
-
}
|
|
250
|
-
if (wait) return json({ error: 'method' }, 405);
|
|
251
|
-
|
|
252
|
-
if (request.method === 'PUT') {
|
|
253
|
-
const subscription = subscriptionOf(body.subscription);
|
|
254
|
-
if (!subscription) return json({ error: 'subscription' }, 400);
|
|
255
|
-
const off = Array.isArray(body.off)
|
|
256
|
-
? body.off
|
|
257
|
-
.filter((t) => typeof t === 'string')
|
|
258
|
-
.slice(0, 100)
|
|
259
|
-
.map((t) => text(t, 100))
|
|
260
|
-
: [];
|
|
261
|
-
const device = { subscription, off, locale: text(body.locale, 20), at: new Date().toISOString() };
|
|
262
|
-
await env.PUSH.put(DEVICE + id, JSON.stringify(device), { expirationTtl: KEEP });
|
|
263
|
-
return json({ ok: true });
|
|
264
|
-
}
|
|
265
|
-
if (request.method === 'DELETE') {
|
|
266
|
-
await Promise.all([env.PUSH.delete(DEVICE + id), env.PUSH.delete(WAIT + id)]);
|
|
267
|
-
return json({ ok: true });
|
|
268
|
-
}
|
|
269
|
-
return json({ error: 'method' }, 405);
|
|
270
|
-
}
|
|
1
|
+
// The editor's notifications on the phone or computer also when the editor is closed: Web Push from the Worker
|
|
2
|
+
// (packages/editor/src/notificationData.ts, packages/editor/pages/editor/sw.js). A device where "På telefonen" is on under "Konto" ›
|
|
3
|
+
// "Notifikationer" signs up here with the browser's push subscription, the types it has turned off and the editor's
|
|
4
|
+
// language; the Worker sends to the browser's push service (Google's, Mozilla's, Apple's), which wakes the editor's
|
|
5
|
+
// service worker, which shows it. Kept in Workers KV (the binding PUSH in wrangler.jsonc):
|
|
6
|
+
// vapid the Worker's own key pair for the push services (VAPID, RFC 8292), made the first time it is needed;
|
|
7
|
+
// a device signed up with another key signs up again when the editor opens
|
|
8
|
+
// device:<id> a device: its subscription, the types turned off and the language
|
|
9
|
+
// wait:<id> the device's last save, until the site is built with it: the commit and the two texts to send
|
|
10
|
+
// waiting until when there may be a wait (so the cron each minute asks KV once, and lists only then)
|
|
11
|
+
// What is sent:
|
|
12
|
+
// - "Hjemmesiden er opdateret med dine ændringer" (type `site`) to the device that saved, when the site has been built
|
|
13
|
+
// with the save (each minute, waitForBuilds: the cron in wrangler.jsonc), or that it was not, after ten minutes;
|
|
14
|
+
// the texts are the editor's, in its language. An editor on screen says it itself and takes the wait back.
|
|
15
|
+
// - an app's notification from its part of the Worker (`notify` in the tools it is given, packages/editor/worker/index.js) to each
|
|
16
|
+
// device with the type on, in the device's language (the app's texts.ts).
|
|
17
|
+
// The payload is encrypted for the device (RFC 8291, aes128gcm); a device the push service no longer knows is removed.
|
|
18
|
+
// GET /api/push { key }: the Worker's public key, for the browser's subscription
|
|
19
|
+
// PUT /api/push { subscription, off, locale }: signs a device up, or updates its choices
|
|
20
|
+
// DELETE /api/push { endpoint }: signs it off ("På telefonen" off, "Log ud")
|
|
21
|
+
// PUT /api/push/wait { endpoint, commit, text, failText, to? }: sends `text` when the site is built with `commit`
|
|
22
|
+
// DELETE /api/push/wait { endpoint }: takes it back (the editor said it itself)
|
|
23
|
+
// Only someone who can save the site (write access to the repository on GitHub) can use them. Without the binding (a
|
|
24
|
+
// preview of a branch) nothing is kept or sent, and the editor's notifications come only while it is open.
|
|
25
|
+
import { config } from '@site/config';
|
|
26
|
+
import { contact } from '@site/worker';
|
|
27
|
+
import { json } from './not-found.js';
|
|
28
|
+
import { mayEdit } from './session.js';
|
|
29
|
+
|
|
30
|
+
const VAPID = 'vapid';
|
|
31
|
+
const DEVICE = 'device:';
|
|
32
|
+
const WAIT = 'wait:';
|
|
33
|
+
const WAITING = 'waiting';
|
|
34
|
+
// How long the editor waits for a save to be on the site (packages/editor/src/deploy.ts GIVE_UP).
|
|
35
|
+
const GIVE_UP = 10 * 60_000;
|
|
36
|
+
// A device not seen (the editor not opened on it) for half a year drops out by itself.
|
|
37
|
+
const KEEP = 180 * 24 * 60 * 60;
|
|
38
|
+
// How long a push service keeps a notification for a device that is off.
|
|
39
|
+
const TTL = 24 * 60 * 60;
|
|
40
|
+
|
|
41
|
+
const encoder = new TextEncoder();
|
|
42
|
+
|
|
43
|
+
function b64url(bytes) {
|
|
44
|
+
let binary = '';
|
|
45
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
46
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fromB64url(text) {
|
|
50
|
+
const binary = atob(text.replace(/-/g, '+').replace(/_/g, '/'));
|
|
51
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const concat = (...parts) => {
|
|
55
|
+
const all = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
56
|
+
let at = 0;
|
|
57
|
+
for (const p of parts) all.set(p, (at += p.length) - p.length);
|
|
58
|
+
return all;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** A device's id: a hash of its endpoint, so the endpoint is not a key. */
|
|
62
|
+
async function idOf(endpoint) {
|
|
63
|
+
const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(endpoint)));
|
|
64
|
+
return b64url(hash.slice(0, 16));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The Worker's key pair as a JWK with the private part, made and kept the first time. */
|
|
68
|
+
async function vapidOf(kv) {
|
|
69
|
+
const kept = await kv.get(VAPID, 'json');
|
|
70
|
+
if (kept) return kept;
|
|
71
|
+
const pair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']);
|
|
72
|
+
const jwk = await crypto.subtle.exportKey('jwk', pair.privateKey);
|
|
73
|
+
await kv.put(VAPID, JSON.stringify(jwk));
|
|
74
|
+
return jwk;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The public key as the browser takes it (an uncompressed point, base64url). */
|
|
78
|
+
const publicKeyOf = (jwk) => b64url(concat(new Uint8Array([4]), fromB64url(jwk.x), fromB64url(jwk.y)));
|
|
79
|
+
|
|
80
|
+
/** The header that says the push comes from this site (VAPID, RFC 8292). */
|
|
81
|
+
async function vapidHeader(jwk, endpoint) {
|
|
82
|
+
const key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
|
|
83
|
+
const part = (o) => b64url(encoder.encode(JSON.stringify(o)));
|
|
84
|
+
const claims = { aud: new URL(endpoint).origin, exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60, sub: `https://${config.domain}` };
|
|
85
|
+
const unsigned = `${part({ typ: 'JWT', alg: 'ES256' })}.${part(claims)}`;
|
|
86
|
+
const signature = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, key, encoder.encode(unsigned));
|
|
87
|
+
return `vapid t=${unsigned}.${b64url(new Uint8Array(signature))}, k=${publicKeyOf(jwk)}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function hkdf(salt, ikm, info, bytes) {
|
|
91
|
+
const key = await crypto.subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
|
|
92
|
+
return new Uint8Array(await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info }, key, bytes * 8));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The payload encrypted for the device (RFC 8291, the content coding aes128gcm of RFC 8188, one record).
|
|
96
|
+
* `salt` and `pair` are for the tests only. */
|
|
97
|
+
export async function encrypt(payload, { p256dh, auth }, { salt = crypto.getRandomValues(new Uint8Array(16)), pair } = {}) {
|
|
98
|
+
const device = fromB64url(p256dh);
|
|
99
|
+
const own = pair ?? (await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']));
|
|
100
|
+
const ownPublic = new Uint8Array(await crypto.subtle.exportKey('raw', own.publicKey));
|
|
101
|
+
const deviceKey = await crypto.subtle.importKey('raw', device, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
|
|
102
|
+
const shared = new Uint8Array(await crypto.subtle.deriveBits({ name: 'ECDH', public: deviceKey }, own.privateKey, 256));
|
|
103
|
+
const ikm = await hkdf(fromB64url(auth), shared, concat(encoder.encode('WebPush: info\0'), device, ownPublic), 32);
|
|
104
|
+
const cek = await hkdf(salt, ikm, encoder.encode('Content-Encoding: aes128gcm\0'), 16);
|
|
105
|
+
const nonce = await hkdf(salt, ikm, encoder.encode('Content-Encoding: nonce\0'), 12);
|
|
106
|
+
const key = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']);
|
|
107
|
+
// The last (and only) record ends with 2.
|
|
108
|
+
const plain = concat(encoder.encode(payload), new Uint8Array([2]));
|
|
109
|
+
const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, plain));
|
|
110
|
+
const size = new Uint8Array(4);
|
|
111
|
+
new DataView(size.buffer).setUint32(0, 4096);
|
|
112
|
+
return concat(salt, size, new Uint8Array([ownPublic.length]), ownPublic, sealed);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Sends a notification ({ title, body, to, tag, error }) to a device; removes the device when its push service no
|
|
116
|
+
* longer knows it. */
|
|
117
|
+
async function send(env, id, device, message) {
|
|
118
|
+
const { endpoint, keys } = device.subscription;
|
|
119
|
+
try {
|
|
120
|
+
const jwk = await vapidOf(env.PUSH);
|
|
121
|
+
const res = await fetch(endpoint, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
authorization: await vapidHeader(jwk, endpoint),
|
|
125
|
+
'content-encoding': 'aes128gcm',
|
|
126
|
+
'content-type': 'application/octet-stream',
|
|
127
|
+
ttl: String(TTL),
|
|
128
|
+
urgency: 'normal',
|
|
129
|
+
},
|
|
130
|
+
body: await encrypt(JSON.stringify({ title: contact.name, ...message }), keys),
|
|
131
|
+
});
|
|
132
|
+
if (res.status === 404 || res.status === 410) await env.PUSH.delete(DEVICE + id);
|
|
133
|
+
else if (!res.ok) console.error(`Notifikationen kunne ikke sendes: ${res.status} ${(await res.text()).slice(0, 200)}`);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error(`Notifikationen kunne ikke sendes: ${e?.message ?? e}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The devices signed up, by their ids. */
|
|
140
|
+
async function devices(kv) {
|
|
141
|
+
const all = [];
|
|
142
|
+
let cursor;
|
|
143
|
+
for (let i = 0; i < 5; i++) {
|
|
144
|
+
const page = await kv.list({ prefix: DEVICE, cursor });
|
|
145
|
+
all.push(...page.keys.map((k) => k.name.slice(DEVICE.length)));
|
|
146
|
+
if (page.list_complete) break;
|
|
147
|
+
cursor = page.cursor;
|
|
148
|
+
}
|
|
149
|
+
const kept = await Promise.all(all.map((id) => kv.get(DEVICE + id, 'json')));
|
|
150
|
+
return all.map((id, i) => ({ id, device: kept[i] })).filter((d) => d.device?.subscription);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Sends an app's notification ({ type, id, text, error, to }, its type as `<app>/<type>`) to each device with the type
|
|
154
|
+
* on, its text in the device's language (`texts`: the app's texts.ts). */
|
|
155
|
+
export async function notifyAll(env, { type, id, text, error, to }, texts = {}) {
|
|
156
|
+
if (!env.PUSH) return;
|
|
157
|
+
for (const { id: device, device: d } of await devices(env.PUSH)) {
|
|
158
|
+
if (d.off?.includes(type)) continue;
|
|
159
|
+
await send(env, device, d, { body: texts[d.locale]?.[text] ?? text, error: !!error, to, tag: `${type}:${id}` });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** What the site was last built from (/editor/version.json, packages/editor/pages/editor/version.json.js). */
|
|
164
|
+
async function builtOf(env) {
|
|
165
|
+
try {
|
|
166
|
+
const res = await env.ASSETS.fetch(new Request('https://assets.local/editor/version.json'));
|
|
167
|
+
return res.ok ? await res.json() : {};
|
|
168
|
+
} catch {
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Whether the build's commit has `commit` in it (it may be built from a later one): asked of GitHub with the Worker's
|
|
174
|
+
* own token (GITHUB_PUBLISH_TOKEN); without it, only the commit itself counts. */
|
|
175
|
+
async function includes(env, build, commit) {
|
|
176
|
+
if (build === commit) return true;
|
|
177
|
+
if (!env.GITHUB_PUBLISH_TOKEN) return false;
|
|
178
|
+
const { owner, name } = config.repo;
|
|
179
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${name}/compare/${commit}...${build}`, {
|
|
180
|
+
headers: { authorization: `Bearer ${env.GITHUB_PUBLISH_TOKEN}`, accept: 'application/vnd.github+json', 'user-agent': `${name}-worker` },
|
|
181
|
+
});
|
|
182
|
+
if (!res.ok) return false;
|
|
183
|
+
const { status } = await res.json().catch(() => ({}));
|
|
184
|
+
return status === 'ahead' || status === 'identical';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Each minute: tells each device waiting for its save whether the site has been built with it (see above). */
|
|
188
|
+
export async function waitForBuilds(env, now = Date.now()) {
|
|
189
|
+
if (!env.PUSH) return;
|
|
190
|
+
const until = Number(await env.PUSH.get(WAITING));
|
|
191
|
+
if (!until || now > until) return;
|
|
192
|
+
const { keys } = await env.PUSH.list({ prefix: WAIT });
|
|
193
|
+
if (!keys.length) return;
|
|
194
|
+
const built = await builtOf(env);
|
|
195
|
+
const builtAt = Date.parse(built.builtAt ?? '');
|
|
196
|
+
for (const { name, metadata: wait } of keys) {
|
|
197
|
+
if (!wait) continue;
|
|
198
|
+
const at = Date.parse(wait.at);
|
|
199
|
+
// A build made before the save cannot have it.
|
|
200
|
+
const online = !!built.commit && builtAt >= at && (await includes(env, built.commit, wait.commit).catch(() => false));
|
|
201
|
+
const late = now - at > GIVE_UP;
|
|
202
|
+
if (!online && !late) continue;
|
|
203
|
+
await env.PUSH.delete(name);
|
|
204
|
+
const device = await env.PUSH.get(DEVICE + wait.device, 'json');
|
|
205
|
+
// Without any version file (a build from before it existed) there is nothing to tell, as in the editor.
|
|
206
|
+
if (!device?.subscription || device.off?.includes('site') || (!online && !built.commit)) continue;
|
|
207
|
+
await send(env, wait.device, device, { body: online ? wait.text : wait.failText, error: !online, to: wait.to, tag: 'site' });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const text = (value, max = 300) => (typeof value === 'string' ? value.slice(0, max) : '');
|
|
212
|
+
const isRoute = (to) => typeof to === 'string' && /^[\w\-/æøåÆØÅ]{1,200}$/.test(to);
|
|
213
|
+
|
|
214
|
+
// The browsers' push services (Chrome's, Firefox's, Safari's, Edge's): the Worker sends to no other address.
|
|
215
|
+
const PUSH_SERVICE =
|
|
216
|
+
/^https:\/\/(fcm\.googleapis\.com|android\.googleapis\.com|updates\.push\.services\.mozilla\.com|[\w-]+(\.[\w-]+)*\.push\.apple\.com|[\w-]+(\.[\w-]+)*\.notify\.windows\.com)\//;
|
|
217
|
+
|
|
218
|
+
function subscriptionOf(value) {
|
|
219
|
+
const { endpoint, keys } = value ?? {};
|
|
220
|
+
if (typeof endpoint !== 'string' || !PUSH_SERVICE.test(endpoint) || endpoint.length > 1000) return null;
|
|
221
|
+
if (typeof keys?.p256dh !== 'string' || typeof keys?.auth !== 'string') return null;
|
|
222
|
+
return { endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The addresses /api/push and /api/push/wait (see above). */
|
|
226
|
+
export async function handlePush(request, env) {
|
|
227
|
+
if (!(await mayEdit(request))) return json({ error: 'forbidden' }, 403);
|
|
228
|
+
if (!env.PUSH) return json({ error: 'unavailable' }, 503);
|
|
229
|
+
const wait = new URL(request.url).pathname.replace(/\/$/, '') === '/api/push/wait';
|
|
230
|
+
if (request.method === 'GET' && !wait) return json({ key: publicKeyOf(await vapidOf(env.PUSH)) });
|
|
231
|
+
const body = await request.json().catch(() => ({}));
|
|
232
|
+
const endpoint = body.endpoint ?? body.subscription?.endpoint;
|
|
233
|
+
if (typeof endpoint !== 'string' || !endpoint) return json({ error: 'endpoint' }, 400);
|
|
234
|
+
const id = await idOf(endpoint);
|
|
235
|
+
|
|
236
|
+
if (wait && request.method === 'PUT') {
|
|
237
|
+
if (typeof body.commit !== 'string' || !/^[0-9a-f]{40}$/.test(body.commit)) return json({ error: 'commit' }, 400);
|
|
238
|
+
const at = new Date().toISOString();
|
|
239
|
+
const metadata = { commit: body.commit, device: id, text: text(body.text), failText: text(body.failText), at };
|
|
240
|
+
if (isRoute(body.to)) metadata.to = body.to;
|
|
241
|
+
// The value is empty: all of it is in the metadata, which the list each minute gives with the key.
|
|
242
|
+
await env.PUSH.put(WAIT + id, '', { metadata, expirationTtl: (2 * GIVE_UP) / 1000 });
|
|
243
|
+
await env.PUSH.put(WAITING, String(Date.now() + GIVE_UP + 2 * 60_000));
|
|
244
|
+
return json({ ok: true });
|
|
245
|
+
}
|
|
246
|
+
if (wait && request.method === 'DELETE') {
|
|
247
|
+
await env.PUSH.delete(WAIT + id);
|
|
248
|
+
return json({ ok: true });
|
|
249
|
+
}
|
|
250
|
+
if (wait) return json({ error: 'method' }, 405);
|
|
251
|
+
|
|
252
|
+
if (request.method === 'PUT') {
|
|
253
|
+
const subscription = subscriptionOf(body.subscription);
|
|
254
|
+
if (!subscription) return json({ error: 'subscription' }, 400);
|
|
255
|
+
const off = Array.isArray(body.off)
|
|
256
|
+
? body.off
|
|
257
|
+
.filter((t) => typeof t === 'string')
|
|
258
|
+
.slice(0, 100)
|
|
259
|
+
.map((t) => text(t, 100))
|
|
260
|
+
: [];
|
|
261
|
+
const device = { subscription, off, locale: text(body.locale, 20), at: new Date().toISOString() };
|
|
262
|
+
await env.PUSH.put(DEVICE + id, JSON.stringify(device), { expirationTtl: KEEP });
|
|
263
|
+
return json({ ok: true });
|
|
264
|
+
}
|
|
265
|
+
if (request.method === 'DELETE') {
|
|
266
|
+
await Promise.all([env.PUSH.delete(DEVICE + id), env.PUSH.delete(WAIT + id)]);
|
|
267
|
+
return json({ ok: true });
|
|
268
|
+
}
|
|
269
|
+
return json({ error: 'method' }, 405);
|
|
270
|
+
}
|