@morphemecms/cms 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -3
  3. package/astro.d.mts +2 -0
  4. package/astro.mjs +162 -0
  5. package/bin/morpheme.mjs +38 -0
  6. package/client.d.ts +10 -0
  7. package/defaults/blockTexts.ts +33 -0
  8. package/defaults/collections.ts +774 -0
  9. package/defaults/colors.ts +125 -0
  10. package/defaults/company.ts +20 -0
  11. package/defaults/editor.ts +178 -0
  12. package/defaults/internalLinks.ts +30 -0
  13. package/defaults/opens.ts +18 -0
  14. package/defaults/popup.tsx +93 -0
  15. package/defaults/privacy.ts +79 -0
  16. package/defaults/rules.ts +450 -0
  17. package/defaults/schema.ts +16 -0
  18. package/defaults/sitedata.ts +129 -0
  19. package/defaults/standard.ts +106 -0
  20. package/defaults/standardPages.ts +87 -0
  21. package/defaults/texts.ts +481 -0
  22. package/demo/README.md +26 -0
  23. package/demo/public/apple-touch-icon.png +0 -0
  24. package/demo/public/editor/apple-touch-icon.png +0 -0
  25. package/demo/public/editor/icon-192.png +0 -0
  26. package/demo/public/editor/icon-512.png +0 -0
  27. package/demo/public/editor/icon-maskable-512.png +0 -0
  28. package/demo/public/favicon.ico +0 -0
  29. package/demo/public/favicon.svg +1 -0
  30. package/demo/public/images/uploads/2026/10/andromeda.webp +0 -0
  31. package/demo/public/images/uploads/2026/10/earth-from-space.webp +0 -0
  32. package/demo/public/images/uploads/2026/10/editor-blog.webp +0 -0
  33. package/demo/public/images/uploads/2026/10/editor-colors.webp +0 -0
  34. package/demo/public/images/uploads/2026/10/editor-home.webp +0 -0
  35. package/demo/public/images/uploads/2026/10/editor-page.webp +0 -0
  36. package/demo/public/images/uploads/2026/10/editor-pages.webp +0 -0
  37. package/demo/public/images/uploads/2026/10/editor-seo.webp +0 -0
  38. package/demo/public/images/uploads/2026/10/editor-start.webp +0 -0
  39. package/demo/public/images/uploads/2026/10/editor-themes.webp +0 -0
  40. package/demo/public/images/uploads/2026/10/moon.webp +0 -0
  41. package/demo/public/images/uploads/2026/10/saturn.webp +0 -0
  42. package/demo/public/logo.svg +1 -0
  43. package/demo/src/content/blog/build-a-page-from-blocks.md +19 -0
  44. package/demo/src/content/blog/colors-everyone-can-read.md +19 -0
  45. package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -0
  46. package/demo/src/content/blog/every-save-is-a-version.md +21 -0
  47. package/demo/src/content/blog/schedule-a-post.md +17 -0
  48. package/demo/src/content/blog/secure-from-day-one.md +17 -0
  49. package/demo/src/content/blog/seo-without-the-guesswork.md +23 -0
  50. package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -0
  51. package/demo/src/content/blog/the-app-store.md +21 -0
  52. package/demo/src/content/pages/about.md +22 -0
  53. package/demo/src/content/pages/app-store.md +30 -0
  54. package/demo/src/content/pages/blocks.md +98 -0
  55. package/demo/src/content/pages/blog-page.md +11 -0
  56. package/demo/src/content/pages/contact.md +16 -0
  57. package/demo/src/content/pages/get-started.md +20 -0
  58. package/demo/src/content/pages/home.md +26 -0
  59. package/demo/src/content/pages/page-not-found.md +11 -0
  60. package/demo/src/content/pages/privacy-policy.md +24 -0
  61. package/demo/src/data/blocks.json +25 -0
  62. package/demo/src/data/booking.json +6 -0
  63. package/demo/src/data/categories/apps.json +6 -0
  64. package/demo/src/data/categories/design.json +6 -0
  65. package/demo/src/data/categories/editing.json +6 -0
  66. package/demo/src/data/categories/security.json +6 -0
  67. package/demo/src/data/categories/seo.json +6 -0
  68. package/demo/src/data/colors.json +75 -0
  69. package/demo/src/data/contact.json +8 -0
  70. package/demo/src/data/cookies.json +21 -0
  71. package/demo/src/data/footer.json +27 -0
  72. package/demo/src/data/menu.json +35 -0
  73. package/demo/src/data/mobile-menu.json +25 -0
  74. package/demo/src/data/popups/booking-bar.json +13 -0
  75. package/demo/src/data/popups/try-the-editor.json +16 -0
  76. package/demo/src/data/standard-pages.json +8 -0
  77. package/demo/src/data/testimonials/sample-bakery.json +10 -0
  78. package/demo/src/data/testimonials/sample-clinic.json +10 -0
  79. package/demo/src/data/testimonials/sample-studio.json +10 -0
  80. package/demo/src/site/colors.ts +11 -0
  81. package/demo/src/site/config.ts +19 -0
  82. package/icons/lucide/LICENSE +43 -0
  83. package/icons/lucide/LICENSE-material-design-icons +20 -0
  84. package/icons/lucide/address-book.svg +1 -0
  85. package/icons/lucide/app-window.svg +1 -0
  86. package/icons/lucide/arrow-counter-clockwise.svg +1 -0
  87. package/icons/lucide/arrow-down.svg +1 -0
  88. package/icons/lucide/arrow-left.svg +1 -0
  89. package/icons/lucide/arrow-right.svg +1 -0
  90. package/icons/lucide/arrow-square-out.svg +1 -0
  91. package/icons/lucide/arrow-u-up-left.svg +1 -0
  92. package/icons/lucide/arrow-u-up-right.svg +1 -0
  93. package/icons/lucide/arrow-up-right.svg +1 -0
  94. package/icons/lucide/arrow-up.svg +1 -0
  95. package/icons/lucide/arrows-down-up.svg +1 -0
  96. package/icons/lucide/arrows-in-line-horizontal.svg +1 -0
  97. package/icons/lucide/arrows-out-line-horizontal.svg +1 -0
  98. package/icons/lucide/article.svg +1 -0
  99. package/icons/lucide/bank.svg +1 -0
  100. package/icons/lucide/bell.svg +1 -0
  101. package/icons/lucide/bicycle.svg +1 -0
  102. package/icons/lucide/binoculars.svg +1 -0
  103. package/icons/lucide/book-open-text.svg +1 -0
  104. package/icons/lucide/book-open.svg +1 -0
  105. package/icons/lucide/browser.svg +1 -0
  106. package/icons/lucide/buildings.svg +1 -0
  107. package/icons/lucide/bus.svg +1 -0
  108. package/icons/lucide/calendar-blank.svg +1 -0
  109. package/icons/lucide/calendar-check.svg +1 -0
  110. package/icons/lucide/car.svg +1 -0
  111. package/icons/lucide/cards.svg +1 -0
  112. package/icons/lucide/caret-down.svg +1 -0
  113. package/icons/lucide/caret-left.svg +1 -0
  114. package/icons/lucide/caret-right.svg +1 -0
  115. package/icons/lucide/caret-up.svg +1 -0
  116. package/icons/lucide/certificate.svg +1 -0
  117. package/icons/lucide/chart-line-up.svg +1 -0
  118. package/icons/lucide/chats.svg +1 -0
  119. package/icons/lucide/check-circle.svg +1 -0
  120. package/icons/lucide/check.svg +1 -0
  121. package/icons/lucide/circle-half.svg +1 -0
  122. package/icons/lucide/circle.svg +1 -0
  123. package/icons/lucide/clock-counter-clockwise.svg +1 -0
  124. package/icons/lucide/clock.svg +1 -0
  125. package/icons/lucide/code.svg +1 -0
  126. package/icons/lucide/coffee.svg +1 -0
  127. package/icons/lucide/columns.svg +1 -0
  128. package/icons/lucide/cookie.svg +1 -0
  129. package/icons/lucide/copy.svg +1 -0
  130. package/icons/lucide/credit-card.svg +1 -0
  131. package/icons/lucide/currency-circle-dollar.svg +1 -0
  132. package/icons/lucide/cursor-click.svg +1 -0
  133. package/icons/lucide/database.svg +1 -0
  134. package/icons/lucide/desktop.svg +1 -0
  135. package/icons/lucide/device-mobile.svg +1 -0
  136. package/icons/lucide/door-open.svg +1 -0
  137. package/icons/lucide/download-simple.svg +1 -0
  138. package/icons/lucide/envelope-simple.svg +1 -0
  139. package/icons/lucide/eye-slash.svg +1 -0
  140. package/icons/lucide/eye.svg +1 -0
  141. package/icons/lucide/file-pdf.svg +1 -0
  142. package/icons/lucide/file-text.svg +1 -0
  143. package/icons/lucide/file-video.svg +1 -0
  144. package/icons/lucide/file-zip.svg +1 -0
  145. package/icons/lucide/flower-lotus.svg +1 -0
  146. package/icons/lucide/folder-simple.svg +1 -0
  147. package/icons/lucide/funnel.svg +1 -0
  148. package/icons/lucide/gauge.svg +1 -0
  149. package/icons/lucide/gear.svg +1 -0
  150. package/icons/lucide/github-logo.svg +1 -0
  151. package/icons/lucide/globe.svg +1 -0
  152. package/icons/lucide/google-logo.svg +1 -0
  153. package/icons/lucide/graduation-cap.svg +1 -0
  154. package/icons/lucide/hammer.svg +1 -0
  155. package/icons/lucide/hand-heart.svg +1 -0
  156. package/icons/lucide/handshake.svg +1 -0
  157. package/icons/lucide/hard-drives.svg +1 -0
  158. package/icons/lucide/heart.svg +1 -0
  159. package/icons/lucide/house.svg +1 -0
  160. package/icons/lucide/image-square.svg +1 -0
  161. package/icons/lucide/image.svg +1 -0
  162. package/icons/lucide/info.svg +1 -0
  163. package/icons/lucide/keyboard.svg +1 -0
  164. package/icons/lucide/leaf.svg +1 -0
  165. package/icons/lucide/lifebuoy.svg +1 -0
  166. package/icons/lucide/lightbulb.svg +1 -0
  167. package/icons/lucide/link-break.svg +1 -0
  168. package/icons/lucide/link-simple.svg +1 -0
  169. package/icons/lucide/list-bullets.svg +1 -0
  170. package/icons/lucide/list-checks.svg +1 -0
  171. package/icons/lucide/list-numbers.svg +1 -0
  172. package/icons/lucide/list.svg +1 -0
  173. package/icons/lucide/lock-simple-open.svg +1 -0
  174. package/icons/lucide/lock-simple.svg +1 -0
  175. package/icons/lucide/magnifying-glass.svg +1 -0
  176. package/icons/lucide/map-pin.svg +1 -0
  177. package/icons/lucide/map-trifold.svg +1 -0
  178. package/icons/lucide/medal.svg +1 -0
  179. package/icons/lucide/megaphone.svg +1 -0
  180. package/icons/lucide/minus.svg +1 -0
  181. package/icons/lucide/moon.svg +1 -0
  182. package/icons/lucide/newspaper.svg +1 -0
  183. package/icons/lucide/pack.json +23 -0
  184. package/icons/lucide/paint-brush-broad.svg +1 -0
  185. package/icons/lucide/palette.svg +1 -0
  186. package/icons/lucide/paper-plane-tilt.svg +1 -0
  187. package/icons/lucide/pencil-simple.svg +1 -0
  188. package/icons/lucide/phone.svg +1 -0
  189. package/icons/lucide/play-fill.svg +1 -0
  190. package/icons/lucide/plus.svg +1 -0
  191. package/icons/lucide/prohibit.svg +1 -0
  192. package/icons/lucide/puzzle-piece.svg +1 -0
  193. package/icons/lucide/question.svg +1 -0
  194. package/icons/lucide/quotes.svg +1 -0
  195. package/icons/lucide/rows.svg +1 -0
  196. package/icons/lucide/ruler.svg +1 -0
  197. package/icons/lucide/scan.svg +1 -0
  198. package/icons/lucide/shapes.svg +1 -0
  199. package/icons/lucide/share-network.svg +1 -0
  200. package/icons/lucide/shield-check.svg +1 -0
  201. package/icons/lucide/sidebar-simple.svg +1 -0
  202. package/icons/lucide/sign-out.svg +1 -0
  203. package/icons/lucide/signpost.svg +1 -0
  204. package/icons/lucide/sliders-horizontal.svg +1 -0
  205. package/icons/lucide/smiley.svg +1 -0
  206. package/icons/lucide/sparkle.svg +1 -0
  207. package/icons/lucide/square-half-bottom.svg +1 -0
  208. package/icons/lucide/square.svg +1 -0
  209. package/icons/lucide/squares-four.svg +1 -0
  210. package/icons/lucide/sticker.svg +1 -0
  211. package/icons/lucide/storefront.svg +1 -0
  212. package/icons/lucide/sun.svg +1 -0
  213. package/icons/lucide/swatches.svg +1 -0
  214. package/icons/lucide/tag.svg +1 -0
  215. package/icons/lucide/text-aa.svg +1 -0
  216. package/icons/lucide/text-align-left.svg +1 -0
  217. package/icons/lucide/text-b.svg +1 -0
  218. package/icons/lucide/text-h-one.svg +1 -0
  219. package/icons/lucide/text-h-three.svg +1 -0
  220. package/icons/lucide/text-h-two.svg +1 -0
  221. package/icons/lucide/text-italic.svg +1 -0
  222. package/icons/lucide/text-t.svg +1 -0
  223. package/icons/lucide/ticket.svg +1 -0
  224. package/icons/lucide/toggle-right.svg +1 -0
  225. package/icons/lucide/train.svg +1 -0
  226. package/icons/lucide/trash.svg +1 -0
  227. package/icons/lucide/upload-simple.svg +1 -0
  228. package/icons/lucide/user-circle.svg +1 -0
  229. package/icons/lucide/user.svg +1 -0
  230. package/icons/lucide/users-three.svg +1 -0
  231. package/icons/lucide/users.svg +1 -0
  232. package/icons/lucide/video-camera.svg +1 -0
  233. package/icons/lucide/warning-circle.svg +1 -0
  234. package/icons/lucide/warning.svg +1 -0
  235. package/icons/lucide/wave-sine.svg +1 -0
  236. package/icons/lucide/wheelchair.svg +1 -0
  237. package/icons/lucide/wrench.svg +1 -0
  238. package/icons/lucide/x.svg +1 -0
  239. package/icons/material-symbols/LICENSE +201 -0
  240. package/icons/material-symbols/LICENSE-material-design-icons +20 -0
  241. package/icons/material-symbols/address-book.svg +1 -0
  242. package/icons/material-symbols/app-window.svg +1 -0
  243. package/icons/material-symbols/arrow-counter-clockwise.svg +1 -0
  244. package/icons/material-symbols/arrow-down.svg +1 -0
  245. package/icons/material-symbols/arrow-left.svg +1 -0
  246. package/icons/material-symbols/arrow-right.svg +1 -0
  247. package/icons/material-symbols/arrow-square-out.svg +1 -0
  248. package/icons/material-symbols/arrow-u-up-left.svg +1 -0
  249. package/icons/material-symbols/arrow-u-up-right.svg +1 -0
  250. package/icons/material-symbols/arrow-up-right.svg +1 -0
  251. package/icons/material-symbols/arrow-up.svg +1 -0
  252. package/icons/material-symbols/arrows-down-up.svg +1 -0
  253. package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -0
  254. package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -0
  255. package/icons/material-symbols/article.svg +1 -0
  256. package/icons/material-symbols/bank.svg +1 -0
  257. package/icons/material-symbols/bell.svg +1 -0
  258. package/icons/material-symbols/bicycle.svg +1 -0
  259. package/icons/material-symbols/binoculars.svg +1 -0
  260. package/icons/material-symbols/book-open-text.svg +1 -0
  261. package/icons/material-symbols/book-open.svg +1 -0
  262. package/icons/material-symbols/browser.svg +1 -0
  263. package/icons/material-symbols/buildings.svg +1 -0
  264. package/icons/material-symbols/bus.svg +1 -0
  265. package/icons/material-symbols/calendar-blank.svg +1 -0
  266. package/icons/material-symbols/calendar-check.svg +1 -0
  267. package/icons/material-symbols/car.svg +1 -0
  268. package/icons/material-symbols/cards.svg +1 -0
  269. package/icons/material-symbols/caret-down.svg +1 -0
  270. package/icons/material-symbols/caret-left.svg +1 -0
  271. package/icons/material-symbols/caret-right.svg +1 -0
  272. package/icons/material-symbols/caret-up.svg +1 -0
  273. package/icons/material-symbols/certificate.svg +1 -0
  274. package/icons/material-symbols/chart-line-up.svg +1 -0
  275. package/icons/material-symbols/chats.svg +1 -0
  276. package/icons/material-symbols/check-circle.svg +1 -0
  277. package/icons/material-symbols/check.svg +1 -0
  278. package/icons/material-symbols/circle-half.svg +1 -0
  279. package/icons/material-symbols/circle.svg +1 -0
  280. package/icons/material-symbols/clock-counter-clockwise.svg +1 -0
  281. package/icons/material-symbols/clock.svg +1 -0
  282. package/icons/material-symbols/code.svg +1 -0
  283. package/icons/material-symbols/coffee.svg +1 -0
  284. package/icons/material-symbols/columns.svg +1 -0
  285. package/icons/material-symbols/cookie.svg +1 -0
  286. package/icons/material-symbols/copy.svg +1 -0
  287. package/icons/material-symbols/credit-card.svg +1 -0
  288. package/icons/material-symbols/currency-circle-dollar.svg +1 -0
  289. package/icons/material-symbols/cursor-click.svg +1 -0
  290. package/icons/material-symbols/database.svg +1 -0
  291. package/icons/material-symbols/desktop.svg +1 -0
  292. package/icons/material-symbols/device-mobile.svg +1 -0
  293. package/icons/material-symbols/door-open.svg +1 -0
  294. package/icons/material-symbols/download-simple.svg +1 -0
  295. package/icons/material-symbols/envelope-simple.svg +1 -0
  296. package/icons/material-symbols/eye-slash.svg +1 -0
  297. package/icons/material-symbols/eye.svg +1 -0
  298. package/icons/material-symbols/file-pdf.svg +1 -0
  299. package/icons/material-symbols/file-text.svg +1 -0
  300. package/icons/material-symbols/file-video.svg +1 -0
  301. package/icons/material-symbols/file-zip.svg +1 -0
  302. package/icons/material-symbols/flower-lotus.svg +1 -0
  303. package/icons/material-symbols/folder-simple.svg +1 -0
  304. package/icons/material-symbols/funnel.svg +1 -0
  305. package/icons/material-symbols/gauge.svg +1 -0
  306. package/icons/material-symbols/gear.svg +1 -0
  307. package/icons/material-symbols/github-logo.svg +1 -0
  308. package/icons/material-symbols/globe.svg +1 -0
  309. package/icons/material-symbols/google-logo.svg +1 -0
  310. package/icons/material-symbols/graduation-cap.svg +1 -0
  311. package/icons/material-symbols/hammer.svg +1 -0
  312. package/icons/material-symbols/hand-heart.svg +1 -0
  313. package/icons/material-symbols/handshake.svg +1 -0
  314. package/icons/material-symbols/hard-drives.svg +1 -0
  315. package/icons/material-symbols/heart.svg +1 -0
  316. package/icons/material-symbols/house.svg +1 -0
  317. package/icons/material-symbols/image-square.svg +1 -0
  318. package/icons/material-symbols/image.svg +1 -0
  319. package/icons/material-symbols/info.svg +1 -0
  320. package/icons/material-symbols/keyboard.svg +1 -0
  321. package/icons/material-symbols/leaf.svg +1 -0
  322. package/icons/material-symbols/lifebuoy.svg +1 -0
  323. package/icons/material-symbols/lightbulb.svg +1 -0
  324. package/icons/material-symbols/link-break.svg +1 -0
  325. package/icons/material-symbols/link-simple.svg +1 -0
  326. package/icons/material-symbols/list-bullets.svg +1 -0
  327. package/icons/material-symbols/list-checks.svg +1 -0
  328. package/icons/material-symbols/list-numbers.svg +1 -0
  329. package/icons/material-symbols/list.svg +1 -0
  330. package/icons/material-symbols/lock-simple-open.svg +1 -0
  331. package/icons/material-symbols/lock-simple.svg +1 -0
  332. package/icons/material-symbols/magnifying-glass.svg +1 -0
  333. package/icons/material-symbols/map-pin.svg +1 -0
  334. package/icons/material-symbols/map-trifold.svg +1 -0
  335. package/icons/material-symbols/medal.svg +1 -0
  336. package/icons/material-symbols/megaphone.svg +1 -0
  337. package/icons/material-symbols/minus.svg +1 -0
  338. package/icons/material-symbols/moon.svg +1 -0
  339. package/icons/material-symbols/newspaper.svg +1 -0
  340. package/icons/material-symbols/pack.json +29 -0
  341. package/icons/material-symbols/paint-brush-broad.svg +1 -0
  342. package/icons/material-symbols/palette.svg +1 -0
  343. package/icons/material-symbols/paper-plane-tilt.svg +1 -0
  344. package/icons/material-symbols/pencil-simple.svg +1 -0
  345. package/icons/material-symbols/phone.svg +1 -0
  346. package/icons/material-symbols/play-fill.svg +1 -0
  347. package/icons/material-symbols/plus.svg +1 -0
  348. package/icons/material-symbols/prohibit.svg +1 -0
  349. package/icons/material-symbols/puzzle-piece.svg +1 -0
  350. package/icons/material-symbols/question.svg +1 -0
  351. package/icons/material-symbols/quotes.svg +1 -0
  352. package/icons/material-symbols/rows.svg +1 -0
  353. package/icons/material-symbols/ruler.svg +1 -0
  354. package/icons/material-symbols/scan.svg +1 -0
  355. package/icons/material-symbols/shapes.svg +1 -0
  356. package/icons/material-symbols/share-network.svg +1 -0
  357. package/icons/material-symbols/shield-check.svg +1 -0
  358. package/icons/material-symbols/sidebar-simple.svg +1 -0
  359. package/icons/material-symbols/sign-out.svg +1 -0
  360. package/icons/material-symbols/signpost.svg +1 -0
  361. package/icons/material-symbols/sliders-horizontal.svg +1 -0
  362. package/icons/material-symbols/smiley.svg +1 -0
  363. package/icons/material-symbols/sparkle.svg +1 -0
  364. package/icons/material-symbols/square-half-bottom.svg +1 -0
  365. package/icons/material-symbols/square.svg +1 -0
  366. package/icons/material-symbols/squares-four.svg +1 -0
  367. package/icons/material-symbols/sticker.svg +1 -0
  368. package/icons/material-symbols/storefront.svg +1 -0
  369. package/icons/material-symbols/sun.svg +1 -0
  370. package/icons/material-symbols/swatches.svg +1 -0
  371. package/icons/material-symbols/tag.svg +1 -0
  372. package/icons/material-symbols/text-aa.svg +1 -0
  373. package/icons/material-symbols/text-align-left.svg +1 -0
  374. package/icons/material-symbols/text-b.svg +1 -0
  375. package/icons/material-symbols/text-h-one.svg +1 -0
  376. package/icons/material-symbols/text-h-three.svg +1 -0
  377. package/icons/material-symbols/text-h-two.svg +1 -0
  378. package/icons/material-symbols/text-italic.svg +1 -0
  379. package/icons/material-symbols/text-t.svg +1 -0
  380. package/icons/material-symbols/ticket.svg +1 -0
  381. package/icons/material-symbols/toggle-right.svg +1 -0
  382. package/icons/material-symbols/train.svg +1 -0
  383. package/icons/material-symbols/trash.svg +1 -0
  384. package/icons/material-symbols/upload-simple.svg +1 -0
  385. package/icons/material-symbols/user-circle.svg +1 -0
  386. package/icons/material-symbols/user.svg +1 -0
  387. package/icons/material-symbols/users-three.svg +1 -0
  388. package/icons/material-symbols/users.svg +1 -0
  389. package/icons/material-symbols/video-camera.svg +1 -0
  390. package/icons/material-symbols/warning-circle.svg +1 -0
  391. package/icons/material-symbols/warning.svg +1 -0
  392. package/icons/material-symbols/wave-sine.svg +1 -0
  393. package/icons/material-symbols/wheelchair.svg +1 -0
  394. package/icons/material-symbols/wrench.svg +1 -0
  395. package/icons/material-symbols/x.svg +1 -0
  396. package/icons/phosphor/LICENSE +21 -0
  397. package/icons/phosphor/address-book.svg +1 -0
  398. package/icons/phosphor/app-window.svg +1 -0
  399. package/icons/phosphor/arrow-counter-clockwise.svg +1 -0
  400. package/icons/phosphor/arrow-down.svg +1 -0
  401. package/icons/phosphor/arrow-left.svg +1 -0
  402. package/icons/phosphor/arrow-right.svg +1 -0
  403. package/icons/phosphor/arrow-square-out.svg +1 -0
  404. package/icons/phosphor/arrow-u-up-left.svg +1 -0
  405. package/icons/phosphor/arrow-u-up-right.svg +1 -0
  406. package/icons/phosphor/arrow-up-right.svg +1 -0
  407. package/icons/phosphor/arrow-up.svg +1 -0
  408. package/icons/phosphor/arrows-down-up.svg +1 -0
  409. package/icons/phosphor/arrows-in-line-horizontal.svg +1 -0
  410. package/icons/phosphor/arrows-out-line-horizontal.svg +1 -0
  411. package/icons/phosphor/article.svg +1 -0
  412. package/icons/phosphor/bank.svg +1 -0
  413. package/icons/phosphor/bell.svg +1 -0
  414. package/icons/phosphor/bicycle.svg +1 -0
  415. package/icons/phosphor/binoculars.svg +1 -0
  416. package/icons/phosphor/book-open-text.svg +1 -0
  417. package/icons/phosphor/book-open.svg +1 -0
  418. package/icons/phosphor/browser.svg +1 -0
  419. package/icons/phosphor/buildings.svg +1 -0
  420. package/icons/phosphor/bus.svg +1 -0
  421. package/icons/phosphor/calendar-blank.svg +1 -0
  422. package/icons/phosphor/calendar-check.svg +1 -0
  423. package/icons/phosphor/car.svg +1 -0
  424. package/icons/phosphor/cards.svg +1 -0
  425. package/icons/phosphor/caret-down.svg +1 -0
  426. package/icons/phosphor/caret-left.svg +1 -0
  427. package/icons/phosphor/caret-right.svg +1 -0
  428. package/icons/phosphor/caret-up.svg +1 -0
  429. package/icons/phosphor/certificate.svg +1 -0
  430. package/icons/phosphor/chart-line-up.svg +1 -0
  431. package/icons/phosphor/chats.svg +1 -0
  432. package/icons/phosphor/check-circle.svg +1 -0
  433. package/icons/phosphor/check.svg +1 -0
  434. package/icons/phosphor/circle-half.svg +1 -0
  435. package/icons/phosphor/circle.svg +1 -0
  436. package/icons/phosphor/clock-counter-clockwise.svg +1 -0
  437. package/icons/phosphor/clock.svg +1 -0
  438. package/icons/phosphor/code.svg +1 -0
  439. package/icons/phosphor/coffee.svg +1 -0
  440. package/icons/phosphor/columns.svg +1 -0
  441. package/icons/phosphor/cookie.svg +1 -0
  442. package/icons/phosphor/copy.svg +1 -0
  443. package/icons/phosphor/credit-card.svg +1 -0
  444. package/icons/phosphor/currency-circle-dollar.svg +1 -0
  445. package/icons/phosphor/cursor-click.svg +1 -0
  446. package/icons/phosphor/database.svg +1 -0
  447. package/icons/phosphor/desktop.svg +1 -0
  448. package/icons/phosphor/device-mobile.svg +1 -0
  449. package/icons/phosphor/door-open.svg +1 -0
  450. package/icons/phosphor/download-simple.svg +1 -0
  451. package/icons/phosphor/envelope-simple.svg +1 -0
  452. package/icons/phosphor/eye-slash.svg +1 -0
  453. package/icons/phosphor/eye.svg +1 -0
  454. package/icons/phosphor/file-pdf.svg +1 -0
  455. package/icons/phosphor/file-text.svg +1 -0
  456. package/icons/phosphor/file-video.svg +1 -0
  457. package/icons/phosphor/file-zip.svg +1 -0
  458. package/icons/phosphor/flower-lotus.svg +1 -0
  459. package/icons/phosphor/folder-simple.svg +1 -0
  460. package/icons/phosphor/funnel.svg +1 -0
  461. package/icons/phosphor/gauge.svg +1 -0
  462. package/icons/phosphor/gear.svg +1 -0
  463. package/icons/phosphor/github-logo.svg +1 -0
  464. package/icons/phosphor/globe.svg +1 -0
  465. package/icons/phosphor/google-logo.svg +1 -0
  466. package/icons/phosphor/graduation-cap.svg +1 -0
  467. package/icons/phosphor/hammer.svg +1 -0
  468. package/icons/phosphor/hand-heart.svg +1 -0
  469. package/icons/phosphor/handshake.svg +1 -0
  470. package/icons/phosphor/hard-drives.svg +1 -0
  471. package/icons/phosphor/heart.svg +1 -0
  472. package/icons/phosphor/house.svg +1 -0
  473. package/icons/phosphor/image-square.svg +1 -0
  474. package/icons/phosphor/image.svg +1 -0
  475. package/icons/phosphor/info.svg +1 -0
  476. package/icons/phosphor/keyboard.svg +1 -0
  477. package/icons/phosphor/leaf.svg +1 -0
  478. package/icons/phosphor/lifebuoy.svg +1 -0
  479. package/icons/phosphor/lightbulb.svg +1 -0
  480. package/icons/phosphor/link-break.svg +1 -0
  481. package/icons/phosphor/link-simple.svg +1 -0
  482. package/icons/phosphor/list-bullets.svg +1 -0
  483. package/icons/phosphor/list-checks.svg +1 -0
  484. package/icons/phosphor/list-numbers.svg +1 -0
  485. package/icons/phosphor/list.svg +1 -0
  486. package/icons/phosphor/lock-simple-open.svg +1 -0
  487. package/icons/phosphor/lock-simple.svg +1 -0
  488. package/icons/phosphor/magnifying-glass.svg +1 -0
  489. package/icons/phosphor/map-pin.svg +1 -0
  490. package/icons/phosphor/map-trifold.svg +1 -0
  491. package/icons/phosphor/medal.svg +1 -0
  492. package/icons/phosphor/megaphone.svg +1 -0
  493. package/icons/phosphor/minus.svg +1 -0
  494. package/icons/phosphor/moon.svg +1 -0
  495. package/icons/phosphor/newspaper.svg +1 -0
  496. package/icons/phosphor/pack.json +8 -0
  497. package/icons/phosphor/paint-brush-broad.svg +1 -0
  498. package/icons/phosphor/palette.svg +1 -0
  499. package/icons/phosphor/paper-plane-tilt.svg +1 -0
  500. package/icons/phosphor/pencil-simple.svg +1 -0
  501. package/icons/phosphor/phone.svg +1 -0
  502. package/icons/phosphor/play-fill.svg +1 -0
  503. package/icons/phosphor/plus.svg +1 -0
  504. package/icons/phosphor/prohibit.svg +1 -0
  505. package/icons/phosphor/puzzle-piece.svg +1 -0
  506. package/icons/phosphor/question.svg +1 -0
  507. package/icons/phosphor/quotes.svg +1 -0
  508. package/icons/phosphor/rows.svg +1 -0
  509. package/icons/phosphor/ruler.svg +1 -0
  510. package/icons/phosphor/scan.svg +1 -0
  511. package/icons/phosphor/shapes.svg +1 -0
  512. package/icons/phosphor/share-network.svg +1 -0
  513. package/icons/phosphor/shield-check.svg +1 -0
  514. package/icons/phosphor/sidebar-simple.svg +1 -0
  515. package/icons/phosphor/sign-out.svg +1 -0
  516. package/icons/phosphor/signpost.svg +1 -0
  517. package/icons/phosphor/sliders-horizontal.svg +1 -0
  518. package/icons/phosphor/smiley.svg +1 -0
  519. package/icons/phosphor/sparkle.svg +1 -0
  520. package/icons/phosphor/square-half-bottom.svg +1 -0
  521. package/icons/phosphor/square.svg +1 -0
  522. package/icons/phosphor/squares-four.svg +1 -0
  523. package/icons/phosphor/sticker.svg +1 -0
  524. package/icons/phosphor/storefront.svg +1 -0
  525. package/icons/phosphor/sun.svg +1 -0
  526. package/icons/phosphor/swatches.svg +1 -0
  527. package/icons/phosphor/tag.svg +1 -0
  528. package/icons/phosphor/text-aa.svg +1 -0
  529. package/icons/phosphor/text-align-left.svg +1 -0
  530. package/icons/phosphor/text-b.svg +1 -0
  531. package/icons/phosphor/text-h-one.svg +1 -0
  532. package/icons/phosphor/text-h-three.svg +1 -0
  533. package/icons/phosphor/text-h-two.svg +1 -0
  534. package/icons/phosphor/text-italic.svg +1 -0
  535. package/icons/phosphor/text-t.svg +1 -0
  536. package/icons/phosphor/ticket.svg +1 -0
  537. package/icons/phosphor/toggle-right.svg +1 -0
  538. package/icons/phosphor/train.svg +1 -0
  539. package/icons/phosphor/trash.svg +1 -0
  540. package/icons/phosphor/upload-simple.svg +1 -0
  541. package/icons/phosphor/user-circle.svg +1 -0
  542. package/icons/phosphor/user.svg +1 -0
  543. package/icons/phosphor/users-three.svg +1 -0
  544. package/icons/phosphor/users.svg +1 -0
  545. package/icons/phosphor/video-camera.svg +1 -0
  546. package/icons/phosphor/warning-circle.svg +1 -0
  547. package/icons/phosphor/warning.svg +1 -0
  548. package/icons/phosphor/wave-sine.svg +1 -0
  549. package/icons/phosphor/wheelchair.svg +1 -0
  550. package/icons/phosphor/wrench.svg +1 -0
  551. package/icons/phosphor/x.svg +1 -0
  552. package/lib/apps.d.mts +28 -0
  553. package/lib/apps.mjs +139 -0
  554. package/lib/blocks.d.mts +23 -0
  555. package/lib/blocks.mjs +90 -0
  556. package/lib/colors.ts +86 -0
  557. package/lib/contactBar.ts +18 -0
  558. package/lib/cookieCategories.d.mts +17 -0
  559. package/lib/cookieCategories.mjs +28 -0
  560. package/lib/crop.d.mts +34 -0
  561. package/lib/crop.mjs +83 -0
  562. package/lib/csp.d.mts +22 -0
  563. package/lib/csp.mjs +130 -0
  564. package/lib/cspSources.d.mts +24 -0
  565. package/lib/cspSources.mjs +60 -0
  566. package/lib/dynamic-colors.ts +426 -0
  567. package/lib/git.d.mts +24 -0
  568. package/lib/git.mjs +64 -0
  569. package/lib/hooks.ts +51 -0
  570. package/lib/inline.ts +20 -0
  571. package/lib/license-texts/Apache-2.0.txt +191 -0
  572. package/lib/license-texts/BSD-2-Clause.txt +24 -0
  573. package/lib/license-texts/BSD-3-Clause.txt +28 -0
  574. package/lib/license-texts/ISC.txt +15 -0
  575. package/lib/license-texts/MIT.txt +21 -0
  576. package/lib/licenses.d.mts +190 -0
  577. package/lib/licenses.mjs +683 -0
  578. package/lib/markdown.ts +42 -0
  579. package/lib/motion-schemes.d.mts +2 -0
  580. package/lib/motion-schemes.mjs +5 -0
  581. package/lib/motion.ts +131 -0
  582. package/lib/notice.d.mts +3 -0
  583. package/lib/notice.mjs +34 -0
  584. package/lib/scheduled.d.mts +15 -0
  585. package/lib/scheduled.mjs +37 -0
  586. package/lib/snackbar.ts +149 -0
  587. package/lib/status.d.mts +7 -0
  588. package/lib/status.mjs +18 -0
  589. package/lib/svg.d.mts +22 -0
  590. package/lib/svg.mjs +231 -0
  591. package/lib/text-colors.d.mts +11 -0
  592. package/lib/text-colors.mjs +43 -0
  593. package/lib/video-id.d.mts +14 -0
  594. package/lib/video-id.mjs +72 -0
  595. package/lib/video.d.mts +20 -0
  596. package/lib/video.mjs +91 -0
  597. package/package.json +131 -6
  598. package/pages/404.astro +10 -0
  599. package/pages/[...slug].astro +51 -0
  600. package/pages/_headers.txt +31 -0
  601. package/pages/blog/index.astro +10 -0
  602. package/pages/blog/kategori/[category].astro +18 -0
  603. package/pages/editor/code-images.json.js +25 -0
  604. package/pages/editor/index.astro +75 -0
  605. package/pages/editor/manifest.webmanifest.js +23 -0
  606. package/pages/editor/sw.js +33 -0
  607. package/pages/editor/sw.js.js +4 -0
  608. package/pages/editor/thumbnails.json.js +21 -0
  609. package/pages/editor/version.json.js +22 -0
  610. package/pages/headers.js +4 -0
  611. package/pages/index.astro +10 -0
  612. package/pages/ny.astro +6 -0
  613. package/pages/robots.txt.js +3 -0
  614. package/pages/rss.xml.js +20 -0
  615. package/scripts/app-screenshots.mjs +164 -0
  616. package/scripts/app-workers.mjs +6 -0
  617. package/scripts/bin.mjs +12 -0
  618. package/scripts/built-site.mjs +99 -0
  619. package/scripts/check-consent.mjs +113 -0
  620. package/scripts/check-content.mjs +149 -0
  621. package/scripts/check-demo.mjs +24 -0
  622. package/scripts/check-themes.mjs +45 -0
  623. package/scripts/check-worker.mjs +569 -0
  624. package/scripts/compare-html.mjs +81 -0
  625. package/scripts/demo-site.mjs +42 -0
  626. package/scripts/editor-icons.mjs +56 -0
  627. package/scripts/editor-roundtrip.mjs +134 -0
  628. package/scripts/editor-screens.mjs +101 -0
  629. package/scripts/git-env.mjs +9 -0
  630. package/scripts/icon-packs.mjs +151 -0
  631. package/scripts/link-counts.mjs +22 -0
  632. package/scripts/page-colors.mjs +42 -0
  633. package/scripts/releases.d.mts +10 -0
  634. package/scripts/releases.mjs +80 -0
  635. package/scripts/responsive-images.mjs +94 -0
  636. package/scripts/scheduled-list.mjs +15 -0
  637. package/scripts/theme-screenshots.mjs +88 -0
  638. package/scripts/types.mjs +21 -0
  639. package/scripts/video-thumbnails.mjs +89 -0
  640. package/scripts/worker-modules.mjs +34 -0
  641. package/site/appCookies.d.mts +9 -0
  642. package/site/appCookies.mjs +74 -0
  643. package/site/apps.ts +21 -0
  644. package/site/autolinks.d.mts +11 -0
  645. package/site/autolinks.mjs +200 -0
  646. package/site/blockScripts.ts +31 -0
  647. package/site/categories.ts +16 -0
  648. package/site/consent.ts +26 -0
  649. package/site/content.config.ts +84 -0
  650. package/site/content.ts +104 -0
  651. package/site/cookieList.ts +17 -0
  652. package/site/defineSite.d.mts +142 -0
  653. package/site/defineSite.mjs +136 -0
  654. package/site/directives/interact.d.mts +2 -0
  655. package/site/directives/interact.mjs +31 -0
  656. package/site/editor-version.d.mts +1 -0
  657. package/site/editor-version.mjs +26 -0
  658. package/site/fill.ts +13 -0
  659. package/site/format.ts +11 -0
  660. package/site/home.ts +66 -0
  661. package/site/iconPacks.d.mts +20 -0
  662. package/site/iconPacks.mjs +43 -0
  663. package/site/icons.d.mts +693 -0
  664. package/site/icons.mjs +605 -0
  665. package/site/images.d.mts +43 -0
  666. package/site/images.mjs +157 -0
  667. package/site/motion.ts +13 -0
  668. package/site/noindex.d.mts +6 -0
  669. package/site/noindex.mjs +21 -0
  670. package/site/pageColors.ts +110 -0
  671. package/site/popups.ts +98 -0
  672. package/site/schema.ts +189 -0
  673. package/site/sections.ts +3 -0
  674. package/site/seo.ts +28 -0
  675. package/site/site.ts +76 -0
  676. package/site/siteConfig.d.mts +1 -0
  677. package/site/siteConfig.mjs +8 -0
  678. package/site/siteCsp.d.mts +10 -0
  679. package/site/siteCsp.mjs +75 -0
  680. package/site/siteModules.d.mts +7 -0
  681. package/site/siteModules.mjs +27 -0
  682. package/site/standardPages.ts +59 -0
  683. package/site/testimonials.ts +48 -0
  684. package/site/text.ts +43 -0
  685. package/site/theme.d.mts +83 -0
  686. package/site/theme.mjs +472 -0
  687. package/site/tracking.client.ts +225 -0
  688. package/site/tracking.ts +9 -0
  689. package/src/ActionParts.tsx +205 -0
  690. package/src/Advanced.tsx +152 -0
  691. package/src/App.tsx +904 -0
  692. package/src/AppList.tsx +1345 -0
  693. package/src/BlockDialog.tsx +360 -0
  694. package/src/BlockParts.tsx +652 -0
  695. package/src/BlockSettings.tsx +533 -0
  696. package/src/BrandForm.tsx +164 -0
  697. package/src/Colors.tsx +476 -0
  698. package/src/CommunicationParts.tsx +199 -0
  699. package/src/CompanyForm.tsx +209 -0
  700. package/src/Consents.tsx +175 -0
  701. package/src/ContainmentParts.tsx +185 -0
  702. package/src/Crop.tsx +152 -0
  703. package/src/DataForm.tsx +434 -0
  704. package/src/Deleted.tsx +177 -0
  705. package/src/EntryForm.tsx +740 -0
  706. package/src/FileView.tsx +30 -0
  707. package/src/FormFrame.tsx +237 -0
  708. package/src/Keywords.tsx +225 -0
  709. package/src/Library.tsx +1563 -0
  710. package/src/Licenses.tsx +336 -0
  711. package/src/LinkSheet.tsx +138 -0
  712. package/src/MarkdownEditor.tsx +1075 -0
  713. package/src/Media.tsx +677 -0
  714. package/src/NavigationParts.tsx +350 -0
  715. package/src/NavigationRestParts.tsx +247 -0
  716. package/src/Notifications.tsx +59 -0
  717. package/src/PagePicker.tsx +136 -0
  718. package/src/Parts.tsx +991 -0
  719. package/src/PartsShown.tsx +168 -0
  720. package/src/PickerParts.tsx +183 -0
  721. package/src/Redirects.tsx +450 -0
  722. package/src/Screen.tsx +184 -0
  723. package/src/SectionsField.tsx +65 -0
  724. package/src/SelectionParts.tsx +134 -0
  725. package/src/SeoScores.tsx +184 -0
  726. package/src/Sheet.tsx +35 -0
  727. package/src/ThemeStore.tsx +376 -0
  728. package/src/Tokens.tsx +346 -0
  729. package/src/Versions.tsx +214 -0
  730. package/src/addresses.ts +75 -0
  731. package/src/allIcons.ts +7 -0
  732. package/src/appFiles.ts +29 -0
  733. package/src/apps.ts +281 -0
  734. package/src/ask.tsx +123 -0
  735. package/src/blocks.ts +251 -0
  736. package/src/brand.ts +153 -0
  737. package/src/cache.ts +42 -0
  738. package/src/check.ts +285 -0
  739. package/src/checkSite.ts +256 -0
  740. package/src/consents.ts +26 -0
  741. package/src/cookieList.ts +70 -0
  742. package/src/countries.ts +59 -0
  743. package/src/deploy.ts +155 -0
  744. package/src/editor.css +602 -0
  745. package/src/examples.ts +85 -0
  746. package/src/fields.tsx +942 -0
  747. package/src/frontmatter.d.mts +6 -0
  748. package/src/frontmatter.mjs +42 -0
  749. package/src/github.ts +376 -0
  750. package/src/i18n/da-DK/advanced.ts +28 -0
  751. package/src/i18n/da-DK/app.ts +71 -0
  752. package/src/i18n/da-DK/apps.ts +90 -0
  753. package/src/i18n/da-DK/blockSettings.ts +38 -0
  754. package/src/i18n/da-DK/blocks.ts +408 -0
  755. package/src/i18n/da-DK/check.ts +25 -0
  756. package/src/i18n/da-DK/colors.ts +64 -0
  757. package/src/i18n/da-DK/common.ts +118 -0
  758. package/src/i18n/da-DK/company.ts +24 -0
  759. package/src/i18n/da-DK/consents.ts +23 -0
  760. package/src/i18n/da-DK/forms.ts +116 -0
  761. package/src/i18n/da-DK/history.ts +35 -0
  762. package/src/i18n/da-DK/index.ts +49 -0
  763. package/src/i18n/da-DK/keywords.ts +19 -0
  764. package/src/i18n/da-DK/library.ts +135 -0
  765. package/src/i18n/da-DK/licenses.ts +25 -0
  766. package/src/i18n/da-DK/media.ts +79 -0
  767. package/src/i18n/da-DK/parts.ts +696 -0
  768. package/src/i18n/da-DK/preview.ts +13 -0
  769. package/src/i18n/da-DK/redirects.ts +51 -0
  770. package/src/i18n/da-DK/seo.ts +20 -0
  771. package/src/i18n/da-DK/text.ts +35 -0
  772. package/src/i18n/da-DK/theme.ts +41 -0
  773. package/src/i18n/en-US/advanced.ts +32 -0
  774. package/src/i18n/en-US/app.ts +84 -0
  775. package/src/i18n/en-US/apps.ts +134 -0
  776. package/src/i18n/en-US/blockSettings.ts +48 -0
  777. package/src/i18n/en-US/blocks.ts +441 -0
  778. package/src/i18n/en-US/check.ts +31 -0
  779. package/src/i18n/en-US/colors.ts +82 -0
  780. package/src/i18n/en-US/common.ts +144 -0
  781. package/src/i18n/en-US/company.ts +25 -0
  782. package/src/i18n/en-US/consents.ts +25 -0
  783. package/src/i18n/en-US/forms.ts +145 -0
  784. package/src/i18n/en-US/history.ts +43 -0
  785. package/src/i18n/en-US/index.ts +51 -0
  786. package/src/i18n/en-US/keywords.ts +24 -0
  787. package/src/i18n/en-US/library.ts +176 -0
  788. package/src/i18n/en-US/licenses.ts +36 -0
  789. package/src/i18n/en-US/media.ts +80 -0
  790. package/src/i18n/en-US/parts.ts +718 -0
  791. package/src/i18n/en-US/preview.ts +15 -0
  792. package/src/i18n/en-US/redirects.ts +66 -0
  793. package/src/i18n/en-US/seo.ts +26 -0
  794. package/src/i18n/en-US/text.ts +41 -0
  795. package/src/i18n/en-US/theme.ts +41 -0
  796. package/src/i18n/index.ts +124 -0
  797. package/src/icon.tsx +160 -0
  798. package/src/iconPacks.ts +89 -0
  799. package/src/images.ts +163 -0
  800. package/src/licenses.ts +79 -0
  801. package/src/markdown.d.mts +12 -0
  802. package/src/markdown.mjs +204 -0
  803. package/src/nav.ts +68 -0
  804. package/src/notFound.ts +28 -0
  805. package/src/notifications.ts +223 -0
  806. package/src/pagePreview.tsx +204 -0
  807. package/src/r2Caching.ts +55 -0
  808. package/src/redirects.ts +149 -0
  809. package/src/registry.tsx +188 -0
  810. package/src/schema.ts +417 -0
  811. package/src/sections.d.mts +45 -0
  812. package/src/sections.mjs +303 -0
  813. package/src/seo.ts +333 -0
  814. package/src/shared.ts +44 -0
  815. package/src/sidebar.ts +31 -0
  816. package/src/site.ts +317 -0
  817. package/src/standard/Block.astro +19 -0
  818. package/src/standard/BlockSite.astro +18 -0
  819. package/src/standard/accordion/index.tsx +47 -0
  820. package/src/standard/accordion/load.ts +23 -0
  821. package/src/standard/accordion/shared.ts +15 -0
  822. package/src/standard/accordion/view.ts +32 -0
  823. package/src/standard/buttons/fields.ts +98 -0
  824. package/src/standard/buttons/index.tsx +23 -0
  825. package/src/standard/buttons/load.ts +34 -0
  826. package/src/standard/buttons/shared.ts +12 -0
  827. package/src/standard/buttons/view.ts +30 -0
  828. package/src/standard/cards/index.tsx +71 -0
  829. package/src/standard/cards/load.ts +17 -0
  830. package/src/standard/cards/shared.ts +27 -0
  831. package/src/standard/cards/view.ts +29 -0
  832. package/src/standard/checklist/index.tsx +27 -0
  833. package/src/standard/checklist/load.ts +5 -0
  834. package/src/standard/checklist/view.ts +13 -0
  835. package/src/standard/cookies/index.tsx +17 -0
  836. package/src/standard/cookies/list.ts +56 -0
  837. package/src/standard/cookies/load.ts +11 -0
  838. package/src/standard/cookies/shared.ts +20 -0
  839. package/src/standard/cookies/view.ts +28 -0
  840. package/src/standard/faq/index.tsx +29 -0
  841. package/src/standard/faq/load.ts +14 -0
  842. package/src/standard/faq/view.ts +21 -0
  843. package/src/standard/head.ts +23 -0
  844. package/src/standard/hero/fields.ts +48 -0
  845. package/src/standard/hero/index.tsx +57 -0
  846. package/src/standard/hero/load.ts +23 -0
  847. package/src/standard/hero/view.ts +38 -0
  848. package/src/standard/links/index.tsx +41 -0
  849. package/src/standard/links/load.ts +17 -0
  850. package/src/standard/links/view.ts +25 -0
  851. package/src/standard/list/index.tsx +50 -0
  852. package/src/standard/list/load.ts +5 -0
  853. package/src/standard/list/view.ts +18 -0
  854. package/src/standard/load.ts +7 -0
  855. package/src/standard/notice/index.tsx +30 -0
  856. package/src/standard/notice/load.ts +16 -0
  857. package/src/standard/notice/view.ts +10 -0
  858. package/src/standard/points/index.tsx +42 -0
  859. package/src/standard/points/load.ts +5 -0
  860. package/src/standard/points/view.ts +12 -0
  861. package/src/standard/posts/index.tsx +132 -0
  862. package/src/standard/posts/load.ts +36 -0
  863. package/src/standard/posts/shared.ts +21 -0
  864. package/src/standard/posts/view.ts +63 -0
  865. package/src/standard/pricecards/index.tsx +57 -0
  866. package/src/standard/pricecards/load.ts +11 -0
  867. package/src/standard/pricecards/view.ts +50 -0
  868. package/src/standard/sharedTexts.ts +6 -0
  869. package/src/standard/site.ts +124 -0
  870. package/src/standard/siteLooks.ts +19 -0
  871. package/src/standard/sites.ts +13 -0
  872. package/src/standard/steps/index.tsx +28 -0
  873. package/src/standard/steps/load.ts +5 -0
  874. package/src/standard/steps/view.ts +11 -0
  875. package/src/standard/testimonials/index.tsx +87 -0
  876. package/src/standard/testimonials/list.ts +43 -0
  877. package/src/standard/testimonials/load.ts +58 -0
  878. package/src/standard/testimonials/shared.ts +13 -0
  879. package/src/standard/testimonials/view.ts +76 -0
  880. package/src/standard/themeBlocks.ts +13 -0
  881. package/src/standard/video/index.tsx +23 -0
  882. package/src/standard/video/load.ts +10 -0
  883. package/src/standard/video/view.ts +42 -0
  884. package/src/standard/view.tsx +105 -0
  885. package/src/standard/views.ts +37 -0
  886. package/src/store.ts +499 -0
  887. package/src/theme.ts +90 -0
  888. package/src/themeUi.ts +97 -0
  889. package/src/themes.ts +91 -0
  890. package/src/ui.tsx +3402 -0
  891. package/src/virtual.d.ts +5 -0
  892. package/src/when.ts +22 -0
  893. package/starter/.githooks/pre-push +4 -0
  894. package/starter/.nvmrc +1 -0
  895. package/starter/.prettierignore +11 -0
  896. package/starter/.prettierrc.json +7 -0
  897. package/starter/README.md +26 -0
  898. package/starter/astro.config.mjs +5 -0
  899. package/starter/package.json +67 -0
  900. package/starter/public/_redirects +1 -0
  901. package/starter/public/apple-touch-icon.png +0 -0
  902. package/starter/public/editor/apple-touch-icon.png +0 -0
  903. package/starter/public/editor/icon-192.png +0 -0
  904. package/starter/public/editor/icon-512.png +0 -0
  905. package/starter/public/editor/icon-maskable-512.png +0 -0
  906. package/starter/public/favicon.ico +0 -0
  907. package/starter/public/favicon.svg +1 -0
  908. package/starter/public/logo.svg +1 -0
  909. package/starter/src/content/pages/blog.md +8 -0
  910. package/starter/src/content/pages/forside.md +9 -0
  911. package/starter/src/content/pages/kontakt.md +7 -0
  912. package/starter/src/content/pages/privatlivspolitik.md +9 -0
  913. package/starter/src/content/pages/siden-findes-ikke.md +7 -0
  914. package/starter/src/content.config.ts +2 -0
  915. package/starter/src/data/apps.json +1 -0
  916. package/starter/src/data/block-settings.json +3 -0
  917. package/starter/src/data/blocks.json +1 -0
  918. package/starter/src/data/booking.json +6 -0
  919. package/starter/src/data/colors.json +70 -0
  920. package/starter/src/data/contact.json +8 -0
  921. package/starter/src/data/cookies.json +21 -0
  922. package/starter/src/data/csp.json +1 -0
  923. package/starter/src/data/footer.json +15 -0
  924. package/starter/src/data/internal-links.json +4 -0
  925. package/starter/src/data/menu.json +4 -0
  926. package/starter/src/data/mobile-menu.json +4 -0
  927. package/starter/src/data/old-addresses.json +1 -0
  928. package/starter/src/data/redirects.json +1 -0
  929. package/starter/src/data/standard-pages.json +8 -0
  930. package/starter/src/data/theme.json +1 -0
  931. package/starter/src/env.d.ts +2 -0
  932. package/starter/src/site/config.ts +13 -0
  933. package/starter/src/site/worker.js +14 -0
  934. package/starter/tsconfig.json +17 -0
  935. package/starter/worker/apps.js +6 -0
  936. package/starter/wrangler.jsonc +39 -0
  937. package/theme/LICENSE +21 -0
  938. package/theme/blocks/ArrowLink.tsx +14 -0
  939. package/theme/blocks/BlockHead.tsx +26 -0
  940. package/theme/blocks/Panel.tsx +32 -0
  941. package/theme/blocks/Person.tsx +26 -0
  942. package/theme/blocks/accordion/PageRow.tsx +25 -0
  943. package/theme/blocks/accordion/View.tsx +17 -0
  944. package/theme/blocks/buttons/Buttons.tsx +28 -0
  945. package/theme/blocks/buttons/View.tsx +12 -0
  946. package/theme/blocks/cards/LinkCards.tsx +72 -0
  947. package/theme/blocks/cards/View.tsx +20 -0
  948. package/theme/blocks/checklist/Checks.tsx +15 -0
  949. package/theme/blocks/checklist/View.tsx +14 -0
  950. package/theme/blocks/cookies/View.tsx +36 -0
  951. package/theme/blocks/faq/Accordion.tsx +34 -0
  952. package/theme/blocks/faq/View.tsx +16 -0
  953. package/theme/blocks/faq/script.ts +56 -0
  954. package/theme/blocks/hero/View.tsx +44 -0
  955. package/theme/blocks/links/LinkGroups.tsx +24 -0
  956. package/theme/blocks/links/View.tsx +17 -0
  957. package/theme/blocks/list/Rows.tsx +24 -0
  958. package/theme/blocks/list/View.tsx +14 -0
  959. package/theme/blocks/notice/View.tsx +13 -0
  960. package/theme/blocks/points/PointList.tsx +34 -0
  961. package/theme/blocks/points/View.tsx +14 -0
  962. package/theme/blocks/posts/View.tsx +26 -0
  963. package/theme/blocks/pricecards/View.tsx +40 -0
  964. package/theme/blocks/steps/StepList.tsx +23 -0
  965. package/theme/blocks/steps/View.tsx +14 -0
  966. package/theme/blocks/testimonials/List.tsx +46 -0
  967. package/theme/blocks/testimonials/Slider.tsx +44 -0
  968. package/theme/blocks/testimonials/Testimonial.tsx +124 -0
  969. package/theme/blocks/testimonials/View.tsx +21 -0
  970. package/theme/blocks/testimonials/script.ts +52 -0
  971. package/theme/blocks/video/View.tsx +41 -0
  972. package/theme/components/ActionBar.astro +63 -0
  973. package/theme/components/AppSlot.astro +21 -0
  974. package/theme/components/ArrowLink.tsx +21 -0
  975. package/theme/components/BlogIndex.astro +48 -0
  976. package/theme/components/BlogPosts.astro +18 -0
  977. package/theme/components/BlogPosts.tsx +45 -0
  978. package/theme/components/BookButton.tsx +22 -0
  979. package/theme/components/Container.astro +11 -0
  980. package/theme/components/CookieConsent.astro +121 -0
  981. package/theme/components/EndBlock.astro +21 -0
  982. package/theme/components/FontPreload.astro +6 -0
  983. package/theme/components/Footer.astro +85 -0
  984. package/theme/components/Header.astro +186 -0
  985. package/theme/components/Icon.astro +16 -0
  986. package/theme/components/Img.astro +29 -0
  987. package/theme/components/Img.tsx +33 -0
  988. package/theme/components/Inline.astro +11 -0
  989. package/theme/components/Logo.astro +23 -0
  990. package/theme/components/LogoMark.astro +10 -0
  991. package/theme/components/Markdown.astro +17 -0
  992. package/theme/components/MobileMenu.tsx +224 -0
  993. package/theme/components/MobileNav.astro +33 -0
  994. package/theme/components/PageHeader.astro +22 -0
  995. package/theme/components/PageSection.astro +24 -0
  996. package/theme/components/PhoneButton.tsx +11 -0
  997. package/theme/components/Popup.astro +145 -0
  998. package/theme/components/PopupButton.tsx +24 -0
  999. package/theme/components/PopupView.tsx +48 -0
  1000. package/theme/components/PostList.tsx +58 -0
  1001. package/theme/components/PostMeta.tsx +17 -0
  1002. package/theme/components/RichText.astro +28 -0
  1003. package/theme/components/Section.astro +29 -0
  1004. package/theme/components/SectionHeading.tsx +8 -0
  1005. package/theme/components/TextSection.astro +16 -0
  1006. package/theme/components/home/PageBlock.astro +19 -0
  1007. package/theme/components/home/Text.astro +13 -0
  1008. package/theme/components/loadPopup.ts +22 -0
  1009. package/theme/components/loadPosts.ts +16 -0
  1010. package/theme/components/posts.ts +15 -0
  1011. package/theme/editor/looks.mjs +7 -0
  1012. package/theme/editor/previews.tsx +56 -0
  1013. package/theme/layouts/Base.astro +273 -0
  1014. package/theme/layouts/PageLayout.astro +126 -0
  1015. package/theme/layouts/PostLayout.astro +137 -0
  1016. package/theme/screenshots/1-front.jpg +0 -0
  1017. package/theme/screenshots/2-front-dark.jpg +0 -0
  1018. package/theme/screenshots/3-blog.jpg +0 -0
  1019. package/theme/screenshots/4-contact.jpg +0 -0
  1020. package/theme/scripts/motion.ts +29 -0
  1021. package/theme/styles/base.css +320 -0
  1022. package/theme/styles/blocks.css +1808 -0
  1023. package/theme/styles/components.css +206 -0
  1024. package/theme/styles/global.css +1302 -0
  1025. package/theme/styles/ui.css +6544 -0
  1026. package/theme/templates/Blog.astro +38 -0
  1027. package/theme/templates/Category.astro +19 -0
  1028. package/theme/templates/Home.astro +36 -0
  1029. package/theme/templates/New.astro +68 -0
  1030. package/theme/templates/NotFound.astro +117 -0
  1031. package/theme/theme.json +28 -0
  1032. package/theme/ui/Badge.tsx +45 -0
  1033. package/theme/ui/BottomSheet.tsx +148 -0
  1034. package/theme/ui/Button.tsx +79 -0
  1035. package/theme/ui/ButtonGroup.tsx +31 -0
  1036. package/theme/ui/Card.tsx +45 -0
  1037. package/theme/ui/Carousel.tsx +124 -0
  1038. package/theme/ui/Checkbox.tsx +59 -0
  1039. package/theme/ui/Chip.tsx +101 -0
  1040. package/theme/ui/ChipSet.tsx +27 -0
  1041. package/theme/ui/DatePicker.tsx +149 -0
  1042. package/theme/ui/DatePickerContainer.tsx +316 -0
  1043. package/theme/ui/Dialog.tsx +100 -0
  1044. package/theme/ui/Divider.tsx +27 -0
  1045. package/theme/ui/ExposedDropdownMenu.tsx +77 -0
  1046. package/theme/ui/Fab.tsx +88 -0
  1047. package/theme/ui/FabMenu.tsx +67 -0
  1048. package/theme/ui/IconButton.tsx +78 -0
  1049. package/theme/ui/List.tsx +287 -0
  1050. package/theme/ui/LoadingIndicator.tsx +29 -0
  1051. package/theme/ui/Menu.tsx +278 -0
  1052. package/theme/ui/NavigationBar.tsx +62 -0
  1053. package/theme/ui/NavigationDrawer.tsx +80 -0
  1054. package/theme/ui/NavigationRail.tsx +154 -0
  1055. package/theme/ui/ProgressIndicator.tsx +162 -0
  1056. package/theme/ui/RadioButton.tsx +50 -0
  1057. package/theme/ui/Search.tsx +129 -0
  1058. package/theme/ui/SegmentedButton.tsx +49 -0
  1059. package/theme/ui/SideSheet.tsx +54 -0
  1060. package/theme/ui/Slider.tsx +155 -0
  1061. package/theme/ui/Snackbar.tsx +67 -0
  1062. package/theme/ui/SplitButton.tsx +67 -0
  1063. package/theme/ui/Switch.tsx +60 -0
  1064. package/theme/ui/Tabs.tsx +114 -0
  1065. package/theme/ui/TextField.tsx +278 -0
  1066. package/theme/ui/TimePicker.tsx +128 -0
  1067. package/theme/ui/TimePickerContainer.tsx +234 -0
  1068. package/theme/ui/Toolbar.tsx +67 -0
  1069. package/theme/ui/Tooltip.tsx +58 -0
  1070. package/theme/ui/TopAppBar.tsx +77 -0
  1071. package/theme/ui/bottomSheet.script.ts +8 -0
  1072. package/theme/ui/bottomSheet.ts +211 -0
  1073. package/theme/ui/carousel.script.ts +28 -0
  1074. package/theme/ui/carouselLayout.ts +229 -0
  1075. package/theme/ui/checkbox.script.ts +8 -0
  1076. package/theme/ui/datePicker.script.ts +302 -0
  1077. package/theme/ui/dates.ts +185 -0
  1078. package/theme/ui/dialog.ts +48 -0
  1079. package/theme/ui/dropdownMenu.script.ts +166 -0
  1080. package/theme/ui/fabMenu.script.ts +57 -0
  1081. package/theme/ui/navigationRail.script.ts +47 -0
  1082. package/theme/ui/ripple.script.ts +159 -0
  1083. package/theme/ui/search.script.ts +92 -0
  1084. package/theme/ui/sideSheet.ts +48 -0
  1085. package/theme/ui/slider.script.ts +12 -0
  1086. package/theme/ui/sliderUpdate.ts +52 -0
  1087. package/theme/ui/snackbar.script.ts +16 -0
  1088. package/theme/ui/splitButton.script.ts +63 -0
  1089. package/theme/ui/tabs.script.ts +48 -0
  1090. package/theme/ui/textField.script.ts +17 -0
  1091. package/theme/ui/textFieldUpdate.ts +44 -0
  1092. package/theme/ui/timePicker.script.ts +248 -0
  1093. package/theme/ui/toolbar.script.ts +43 -0
  1094. package/theme/ui/tooltip.script.ts +68 -0
  1095. package/worker/consent.js +141 -0
  1096. package/worker/editor-github.js +167 -0
  1097. package/worker/editor-host.js +74 -0
  1098. package/worker/files.js +401 -0
  1099. package/worker/github.js +36 -0
  1100. package/worker/index.js +184 -0
  1101. package/worker/not-found.js +97 -0
  1102. package/worker/oauth.js +95 -0
  1103. package/worker/publish.js +52 -0
  1104. package/worker/push.js +270 -0
  1105. package/worker/r2-status.js +50 -0
  1106. package/worker/session.js +58 -0
@@ -0,0 +1,97 @@
1
+ // The addresses visitors came to that do not exist (the site's 404 page), for "Videresendelser" in the
2
+ // editor (packages/editor/src/notFound.ts, Redirects.tsx), where the site's owner can send them on to a page.
3
+ // Kept in Workers KV (the binding NOT_FOUND in wrangler.jsonc), one key per address: how many times,
4
+ // when first and last, and where the visitor came from. No IP address, no cookie: nothing about the
5
+ // visitor, so it needs no consent. An address not seen for 90 days drops out by itself.
6
+ // Without the binding (a preview of a branch) nothing is kept and the editor says so.
7
+ // GET /api/not-found the addresses, most visits first
8
+ // DELETE /api/not-found { paths: [...] } takes them off the list (sent on, or not worth it)
9
+ // Only someone who can save the site (write access to the repository on GitHub) can read or change it.
10
+ import { mayEdit } from './session.js';
11
+
12
+ const PREFIX = '404:';
13
+ const KEEP = 90 * 24 * 60 * 60;
14
+ // A page: no file ending in the last part (not a picture, a script or /wp-login.php).
15
+ const PAGE = /(^|\/)[^./]*$/;
16
+ // What is not a page of the site, and what robots try on every site.
17
+ const SKIP = /^\/(editor|oauth|api|cdn-cgi|\.well-known|wp-admin|wp-includes|wp-json)(\/|$)/i;
18
+ const ROBOT = /bot|crawl|spider|slurp|headless|python|curl|wget|scan|http/i;
19
+
20
+ /** Whether a 404 is a visitor's, worth keeping: a browser opening a page, on the public site. */
21
+ export function isVisit(request, url) {
22
+ const agent = request.headers.get('user-agent') ?? '';
23
+ return (
24
+ request.method === 'GET' &&
25
+ url.pathname.length <= 300 &&
26
+ PAGE.test(url.pathname) &&
27
+ !SKIP.test(url.pathname) &&
28
+ agent.includes('Mozilla') &&
29
+ !ROBOT.test(agent.replace(/^Mozilla\/[\d.]+ /, '')) &&
30
+ (request.headers.get('accept') ?? '').includes('text/html')
31
+ );
32
+ }
33
+
34
+ /** Counts a visit to an address that does not exist. Never fails the request (KV may say no). */
35
+ export async function recordNotFound(request, url, env) {
36
+ if (!env.NOT_FOUND) return;
37
+ try {
38
+ const key = PREFIX + addressOf(url.pathname);
39
+ const { metadata: was } = await env.NOT_FOUND.getWithMetadata(key);
40
+ const now = new Date().toISOString();
41
+ const metadata = { n: (was?.n ?? 0) + 1, first: was?.first ?? now, last: now, from: cameFrom(request, url) || was?.from || '' };
42
+ await env.NOT_FOUND.put(key, '', { metadata, expirationTtl: KEEP });
43
+ } catch {}
44
+ }
45
+
46
+ // The address as the editor's redirects have it: decoded (/æble/, not /%C3%A6ble/), and with the slash
47
+ // at the end, for /kontakt is sent on by the redirect from /kontakt/ (packages/editor/worker/index.js withSlash).
48
+ function addressOf(pathname) {
49
+ let path = pathname;
50
+ try {
51
+ path = decodeURIComponent(pathname);
52
+ } catch {}
53
+ return path.endsWith('/') ? path : `${path}/`;
54
+ }
55
+
56
+ // Where the visitor came from: a page of the site by its address (a link on it is broken), another
57
+ // site by its name (google.com); browsers send no more than that for another site.
58
+ function cameFrom(request, url) {
59
+ try {
60
+ const from = new URL(request.headers.get('referer') ?? '');
61
+ if (from.host === url.host) return from.pathname.slice(0, 200);
62
+ return from.hostname.replace(/^www\./, '').slice(0, 200);
63
+ } catch {
64
+ return '';
65
+ }
66
+ }
67
+
68
+ export async function handleNotFound(request, env) {
69
+ if (!(await mayEdit(request))) return json({ error: 'forbidden' }, 403);
70
+ if (!env.NOT_FOUND) return json({ error: 'unavailable' }, 503);
71
+ if (request.method === 'GET') return json({ entries: await entries(env.NOT_FOUND) });
72
+ if (request.method === 'DELETE') {
73
+ const body = await request.json().catch(() => ({}));
74
+ const paths = Array.isArray(body.paths) ? body.paths.filter((p) => typeof p === 'string').slice(0, 100) : [];
75
+ await Promise.all(paths.map((p) => env.NOT_FOUND.delete(PREFIX + p)));
76
+ return json({ deleted: paths.length });
77
+ }
78
+ return json({ error: 'method' }, 405);
79
+ }
80
+
81
+ async function entries(kv) {
82
+ const all = [];
83
+ let cursor;
84
+ // At most 5000 addresses; more would be robots.
85
+ for (let i = 0; i < 5; i++) {
86
+ const page = await kv.list({ prefix: PREFIX, cursor });
87
+ for (const { name, metadata } of page.keys)
88
+ if (metadata)
89
+ all.push({ path: name.slice(PREFIX.length), count: metadata.n, first: metadata.first, last: metadata.last, from: metadata.from });
90
+ if (page.list_complete) break;
91
+ cursor = page.cursor;
92
+ }
93
+ return all.sort((a, b) => b.count - a.count || b.last.localeCompare(a.last));
94
+ }
95
+
96
+ export const json = (data, status = 200) =>
97
+ new Response(JSON.stringify(data), { status, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' } });
@@ -0,0 +1,95 @@
1
+ // GitHub sign-in for the editor at /editor/ (packages/editor/src/github.ts). See docs/editor.md.
2
+ // /oauth/auth the editor goes here; we send the browser on to GitHub
3
+ // /oauth/callback GitHub sends the browser back here; we swap the code for a token, keep it in a
4
+ // cookie only the Worker can read (packages/editor/worker/session.js) and go back to /editor/
5
+ // A redirect rather than a popup, so it also works in the editor installed as an app on a phone.
6
+ // Needs the Worker secrets GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET from a GitHub App whose
7
+ // callback URL is https://<hostname>/oauth/callback (installed on this repo only; see docs/editor.md).
8
+ // Who may edit is decided by GitHub: only people with write access to the repository can save.
9
+ import { sessionCookie } from './session.js';
10
+
11
+ const STATE_COOKIE = 'cms_oauth_state';
12
+ // The editor asks for "repo"; anything outside this list is dropped.
13
+ const SCOPES = new Set(['repo', 'public_repo', 'user', 'read:user', 'user:email']);
14
+
15
+ export async function handleOAuth(request, env) {
16
+ const url = new URL(request.url);
17
+ if (url.pathname === '/oauth/auth') return authorize(url, env);
18
+ if (url.pathname === '/oauth/callback') return callback(request, url, env);
19
+ return null;
20
+ }
21
+
22
+ function authorize(url, env) {
23
+ if (!env.GITHUB_CLIENT_ID || !env.GITHUB_CLIENT_SECRET) {
24
+ return finish({ error: 'GitHub-login er ikke sat op endnu (mangler GITHUB_CLIENT_ID/SECRET).' });
25
+ }
26
+ if ((url.searchParams.get('provider') ?? 'github') !== 'github') return finish({ error: 'Ukendt login.' });
27
+ const scope =
28
+ (url.searchParams.get('scope') ?? '')
29
+ .split(/[\s,]+/)
30
+ .filter((s) => SCOPES.has(s))
31
+ .join(',') || 'repo';
32
+ const state = crypto.randomUUID();
33
+ const github = new URL('https://github.com/login/oauth/authorize');
34
+ github.search = new URLSearchParams({
35
+ client_id: env.GITHUB_CLIENT_ID,
36
+ redirect_uri: `${url.origin}/oauth/callback`,
37
+ scope,
38
+ state,
39
+ });
40
+ return new Response(null, {
41
+ status: 302,
42
+ headers: {
43
+ location: github.href,
44
+ 'set-cookie': `${STATE_COOKIE}=${state}; Path=/oauth/; Max-Age=600; HttpOnly; Secure; SameSite=Lax`,
45
+ 'cache-control': 'no-store',
46
+ },
47
+ });
48
+ }
49
+
50
+ async function callback(request, url, env) {
51
+ const code = url.searchParams.get('code');
52
+ const state = url.searchParams.get('state');
53
+ const cookie = request.headers.get('cookie') ?? '';
54
+ const expected = cookie.match(new RegExp(`(?:^|;\\s*)${STATE_COOKIE}=([^;]+)`))?.[1];
55
+ if (!code || !state || state !== expected) {
56
+ return finish({ error: 'Login mislykkedes (ugyldig eller udløbet forespørgsel). Prøv igen.' });
57
+ }
58
+
59
+ const res = await fetch('https://github.com/login/oauth/access_token', {
60
+ method: 'POST',
61
+ headers: { accept: 'application/json', 'content-type': 'application/json', 'user-agent': 'morpheme-cms' },
62
+ body: JSON.stringify({
63
+ client_id: env.GITHUB_CLIENT_ID,
64
+ client_secret: env.GITHUB_CLIENT_SECRET,
65
+ code,
66
+ redirect_uri: `${url.origin}/oauth/callback`,
67
+ }),
68
+ });
69
+ const data = await res.json().catch(() => ({}));
70
+ if (!res.ok || !data.access_token) {
71
+ return finish({ error: `Login mislykkedes: ${data.error_description ?? data.error ?? res.status}` });
72
+ }
73
+ return finish({ token: data.access_token });
74
+ }
75
+
76
+ // The page GitHub's sign-in ends on: with a token, it keeps it for the Worker (the cookie) and opens the
77
+ // editor (from this page, so the cookie, SameSite=Strict, goes with what the editor asks); with an error, it
78
+ // shows it with a way back.
79
+ function finish({ token, error }) {
80
+ const body = error
81
+ ? `<p>${escapeHtml(error)}</p><p><a href="/editor/">Tilbage til editoren</a></p>`
82
+ : `<p>Logger ind…</p><script>location.replace('/editor/');</script>`;
83
+ const html = `<!doctype html><html lang="da"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex"><title>Login</title></head>
84
+ <body style="font-family: system-ui, sans-serif; padding: 2rem">${body}</body></html>`;
85
+ const headers = new Headers({
86
+ 'content-type': 'text/html; charset=utf-8',
87
+ 'cache-control': 'no-store',
88
+ 'set-cookie': `${STATE_COOKIE}=; Path=/oauth/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`,
89
+ });
90
+ if (token) headers.append('set-cookie', sessionCookie(token));
91
+ return new Response(html, { status: error ? 400 : 200, headers });
92
+ }
93
+
94
+ const escapeHtml = (s) =>
95
+ String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
@@ -0,0 +1,52 @@
1
+ // Publishes the pages and blog posts that are Planlagt once their time has come (packages/editor/lib/status.mjs): each hour (the
2
+ // cron in wrangler.jsonc, minute 7) the Worker reads the list the build made of them (/_scheduled.json,
3
+ // packages/editor/scripts/scheduled-list.mjs), and if a time has come, it makes those files Udgivet (their `status` and `publishAt`
4
+ // lines go) in one commit on main (*Udgiv planlagt: blogindlæg "…"*), which builds the site. Nothing is asked of
5
+ // GitHub in an hour with nothing to publish. Needs the Worker secret GITHUB_PUBLISH_TOKEN: a GitHub token with
6
+ // Contents: Read and write on the repository (docs/editor.md, Opsætning; packages/editor/worker/github.js).
7
+ import { commitFiles, readFiles } from './github.js';
8
+ import { isDue, nameOf, published, scheduleOf } from '../lib/scheduled.mjs';
9
+
10
+ /** The address of the build's list; on the site it gives nothing (packages/editor/worker/index.js). */
11
+ export const SCHEDULED_LIST = '/_scheduled.json';
12
+
13
+ export async function publishScheduled(env, now = Date.now()) {
14
+ const due = (await listOf(env)).filter((e) => isDue(e.publishAt, now));
15
+ if (!due.length) return [];
16
+ if (!env.GITHUB_PUBLISH_TOKEN) {
17
+ console.error('Planlagte sider og indlæg kan ikke udgives: Workeren mangler hemmeligheden GITHUB_PUBLISH_TOKEN.');
18
+ return [];
19
+ }
20
+ // The files as they are now (one may have been published or changed since the build).
21
+ const { head, texts } = await readFiles(
22
+ env,
23
+ due.map((e) => e.path),
24
+ );
25
+ const changes = due
26
+ .map((e, i) => ({ path: e.path, text: texts[i] }))
27
+ .filter(({ text }) => {
28
+ const file = scheduleOf(text ?? '');
29
+ return file.status === 'scheduled' && isDue(file.publishAt, now);
30
+ });
31
+ if (!changes.length) return [];
32
+ const names = changes.map((c) => nameOf(c.path));
33
+ await commitFiles(
34
+ env,
35
+ head,
36
+ `Udgiv planlagt: ${names.join(', ')}`,
37
+ changes.map((c) => ({ path: c.path, text: published(c.text) })),
38
+ );
39
+ console.log(`Udgivet: ${names.join(', ')}`);
40
+ return names;
41
+ }
42
+
43
+ // The build's list; none (a build before the list, or a preview) is nothing to publish.
44
+ async function listOf(env) {
45
+ try {
46
+ const res = await env.ASSETS.fetch(new Request(`https://assets.local${SCHEDULED_LIST}`));
47
+ const list = res.ok ? await res.json() : [];
48
+ return Array.isArray(list) ? list : [];
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
package/worker/push.js ADDED
@@ -0,0 +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/notifications.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
+ }
@@ -0,0 +1,50 @@
1
+ // Whether R2 caching (packages/editor/worker/files.js, the binding FILES) can be turned on, for "Indstillinger" › "Avanceret" in the
2
+ // editor (packages/editor/src/Advanced.tsx): the switch writes the bucket into the Worker's settings (wrangler.jsonc),
3
+ // and a deploy with a bucket Cloudflare does not have fails, so it can only be turned on when Cloudflare says the bucket
4
+ // is there (R2 turned on for the account, and the bucket made). Asked of Cloudflare's API with the Worker's own key, a
5
+ // token that may only read R2 (the secret CLOUDFLARE_R2_TOKEN, and CLOUDFLARE_ACCOUNT_ID; docs/editor.md, Opsætning).
6
+ // GET /api/r2?bucket=<name>[&jurisdiction=<eu|fedramp>] { bound, bucket }: whether this Worker has FILES now (the site is built with it), and the
7
+ // bucket at Cloudflare: "ready" (there), "no-bucket", "no-r2" (R2 not turned on), "no-key" (the Worker has no key),
8
+ // "refused" (Cloudflare said no to the key) or "failed". A bucket kept within a jurisdiction is only found in it.
9
+ // Only someone who can save the site (write access to the repository on GitHub) can ask.
10
+ import { json } from './not-found.js';
11
+ import { mayEdit } from './session.js';
12
+
13
+ // Cloudflare's codes for a bucket that is not there and for an account without R2.
14
+ const NO_BUCKET = 10006;
15
+ const NO_R2 = 10042;
16
+ // R2's rule for a bucket's name.
17
+ const NAME = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/;
18
+ // The jurisdictions R2 can keep a bucket within.
19
+ const JURISDICTION = /^(eu|fedramp)$/;
20
+
21
+ export async function handleR2(request, env) {
22
+ if (!(await mayEdit(request))) return json({ error: 'forbidden' }, 403);
23
+ if (request.method !== 'GET') return json({ error: 'method' }, 405);
24
+ const params = new URL(request.url).searchParams;
25
+ const name = params.get('bucket') ?? '';
26
+ const jurisdiction = params.get('jurisdiction');
27
+ if (!NAME.test(name)) return json({ error: 'bucket' }, 400);
28
+ if (jurisdiction !== null && !JURISDICTION.test(jurisdiction)) return json({ error: 'jurisdiction' }, 400);
29
+ return json({ bound: !!env.FILES, bucket: await bucketAt(env, name, jurisdiction) });
30
+ }
31
+
32
+ async function bucketAt(env, name, jurisdiction) {
33
+ const account = env.CLOUDFLARE_ACCOUNT_ID?.trim();
34
+ const token = env.CLOUDFLARE_R2_TOKEN?.trim();
35
+ if (!account || !token) return 'no-key';
36
+ try {
37
+ const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(account)}/r2/buckets/${name}`, {
38
+ headers: { authorization: `Bearer ${token}`, ...(jurisdiction ? { 'cf-r2-jurisdiction': jurisdiction } : {}) },
39
+ });
40
+ const body = await res.json().catch(() => ({}));
41
+ if (res.ok && body.success) return 'ready';
42
+ const codes = (body.errors ?? []).map((e) => e.code);
43
+ if (codes.includes(NO_R2)) return 'no-r2';
44
+ if (codes.includes(NO_BUCKET)) return 'no-bucket';
45
+ if (res.status === 401 || res.status === 403) return 'refused';
46
+ return 'failed';
47
+ } catch {
48
+ return 'failed';
49
+ }
50
+ }
@@ -0,0 +1,58 @@
1
+ // The editor's GitHub sign-in, kept by the Worker: the token is in a cookie only the Worker can read (HttpOnly, on the
2
+ // editor's own address, sent only with what the editor asks the Worker, /api/), never in the browser's storage, so no
3
+ // script can take it away. Set by the sign-in (packages/editor/worker/oauth.js), cleared by "Log ud" (/api/github/logout,
4
+ // packages/editor/worker/editor-github.js) and on the old address (packages/editor/worker/editor-host.js). See docs/editor.md.
5
+ import { config } from '@site/config';
6
+
7
+ const COOKIE = 'editor_session';
8
+ // Until "Log ud": as long as a browser keeps a cookie (400 days).
9
+ const KEEP = 400 * 24 * 60 * 60;
10
+
11
+ /** The header that keeps the token in the browser. */
12
+ export const sessionCookie = (token) => `${COOKIE}=${token}; Path=/api/; Max-Age=${KEEP}; HttpOnly; Secure; SameSite=Strict`;
13
+
14
+ /** The header that removes it. */
15
+ export const noSession = () => `${COOKIE}=; Path=/api/; Max-Age=0; HttpOnly; Secure; SameSite=Strict`;
16
+
17
+ /** The token of the request's sign-in, or null. Only a request from the editor's own page counts: SameSite=Strict
18
+ * still sends the cookie from a page on another address of the same domain (example.dk to editor.…). */
19
+ export function sessionToken(request) {
20
+ const url = new URL(request.url);
21
+ const site = request.headers.get('sec-fetch-site');
22
+ const origin = request.headers.get('origin');
23
+ if (site ? site !== 'same-origin' : origin && origin !== url.origin) return null;
24
+ return request.headers.get('cookie')?.match(new RegExp(`(?:^|;\\s*)${COOKIE}=([^;]+)`))?.[1] ?? null;
25
+ }
26
+
27
+ // What GitHub said each token may do in the repository (by a hash of the token), for ten minutes, so not every request
28
+ // asks.
29
+ const known = new Map();
30
+ const keyOf = async (token) => [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(token)))].join('.');
31
+
32
+ /** What the request's sign-in may do in the site's repository: read it and save to it (`push`), as GitHub says; null
33
+ * without a sign-in or when GitHub no longer knows it. `ask`: fetch for GitHub (packages/editor/worker/files.js counts its requests). */
34
+ export async function access(request, ask = fetch) {
35
+ const token = sessionToken(request);
36
+ if (!token) return null;
37
+ const id = await keyOf(token);
38
+ const was = known.get(id);
39
+ if (was && was.until > Date.now()) return { token, read: was.read, push: was.push };
40
+ const res = await ask(`https://api.github.com/repos/${config.repo.owner}/${config.repo.name}`, {
41
+ headers: { authorization: `Bearer ${token}`, accept: 'application/vnd.github+json', 'user-agent': `${config.repo.name}-worker` },
42
+ });
43
+ // 401: the token is no good (signed out); otherwise GitHub does not show the repository to it (404), or failed.
44
+ if (res.status === 401) return null;
45
+ if (!res.ok) return { token, read: false, push: false };
46
+ const { permissions = {} } = await res.json().catch(() => ({}));
47
+ const may = { read: permissions.pull === true, push: permissions.push === true };
48
+ known.set(id, { ...may, until: Date.now() + 10 * 60_000 });
49
+ return { token, ...may };
50
+ }
51
+
52
+ /** Whether the request comes from someone who can save the site (also an app's own addresses, /api/<app>/, given to it by packages/editor/worker/index.js). */
53
+ export const mayEdit = async (request) => (await access(request))?.push === true;
54
+
55
+ /** Forgets what GitHub said about a token (it was revoked at "Log ud"). */
56
+ export async function forget(token) {
57
+ known.delete(await keyOf(token));
58
+ }