@jant/core 0.6.16 → 0.7.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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Tests for who may answer the last setup question on a provisioned site.
3
+ *
4
+ * A provisioned site already has an owner, so `POST /setup` writes the site's
5
+ * name and language on behalf of that owner. Carrying a session is not enough
6
+ * to be that owner — the session may belong to someone this site has never
7
+ * heard of.
8
+ */
9
+
10
+ import { describe, expect, it, vi } from "vitest";
11
+ import { Hono } from "hono";
12
+ import { createI18n } from "../../../i18n/index.js";
13
+ import { attachSession } from "../../../middleware/session.js";
14
+ import { ONBOARDING_STATUS } from "../../../lib/constants.js";
15
+ import { setupRoutes } from "../setup.js";
16
+ import type { Bindings } from "../../../types.js";
17
+ import type { AppVariables } from "../../../types/app-context.js";
18
+
19
+ type Env = { Bindings: Bindings; Variables: AppVariables };
20
+
21
+ function createMockAuth(signedIn: boolean) {
22
+ return {
23
+ api: {
24
+ getSession: async () => ({
25
+ headers: new Headers(),
26
+ response: signedIn
27
+ ? {
28
+ user: {
29
+ id: "usr_outsider",
30
+ email: "someone@test.com",
31
+ name: "S",
32
+ },
33
+ session: { id: "ses_outsider" },
34
+ }
35
+ : null,
36
+ }),
37
+ },
38
+ } as unknown as AppVariables["auth"];
39
+ }
40
+
41
+ function createProvisionedSetupApp(options: {
42
+ signedIn: boolean;
43
+ isMember: boolean;
44
+ }) {
45
+ const completeSiteSetup = vi.fn(async () => {});
46
+
47
+ const app = new Hono<Env>();
48
+ app.use("*", async (c, next) => {
49
+ c.env = {} as Bindings;
50
+ c.set("auth", createMockAuth(options.signedIn));
51
+ c.set("services", {
52
+ settings: {
53
+ getOnboardingStatus: async () => ONBOARDING_STATUS.PROVISIONED,
54
+ get: async () => "My Blog",
55
+ },
56
+ bootstrap: { completeSiteSetup },
57
+ siteMembers: {
58
+ get: async () =>
59
+ options.isMember
60
+ ? {
61
+ createdAt: 0,
62
+ role: "owner",
63
+ siteId: "sit_test",
64
+ updatedAt: 0,
65
+ userId: "usr_outsider",
66
+ }
67
+ : null,
68
+ },
69
+ } as unknown as AppVariables["services"]);
70
+ c.set("currentSite", {
71
+ createdAt: 0,
72
+ id: "sit_test",
73
+ key: "default",
74
+ status: "active",
75
+ updatedAt: 0,
76
+ });
77
+ c.set("appConfig", {
78
+ siteName: "Jant",
79
+ sitePathPrefix: "",
80
+ siteLanguage: "en",
81
+ } as AppVariables["appConfig"]);
82
+ c.set("lang", "en");
83
+ c.set("i18n", createI18n("en"));
84
+ await next();
85
+ });
86
+ app.use("*", attachSession());
87
+ app.route("/", setupRoutes);
88
+
89
+ return { app, completeSiteSetup };
90
+ }
91
+
92
+ function postLanguage(app: Hono<Env>) {
93
+ return app.request("/setup", {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify({ contentLanguage: "zh-Hans" }),
97
+ });
98
+ }
99
+
100
+ describe("POST /setup on a provisioned site", () => {
101
+ it("refuses a session that is not a member of this site", async () => {
102
+ const { app, completeSiteSetup } = createProvisionedSetupApp({
103
+ signedIn: true,
104
+ isMember: false,
105
+ });
106
+
107
+ const res = await postLanguage(app);
108
+
109
+ await expect(res.text()).resolves.toContain("/signin");
110
+ expect(completeSiteSetup).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it("lets the site's own owner answer", async () => {
114
+ const { app, completeSiteSetup } = createProvisionedSetupApp({
115
+ signedIn: true,
116
+ isMember: true,
117
+ });
118
+
119
+ await postLanguage(app);
120
+
121
+ expect(completeSiteSetup).toHaveBeenCalledOnce();
122
+ });
123
+ });
124
+
125
+ describe("GET /setup on a provisioned site", () => {
126
+ it("sends a non-member to the site instead of showing the form", async () => {
127
+ const { app } = createProvisionedSetupApp({
128
+ signedIn: true,
129
+ isMember: false,
130
+ });
131
+
132
+ const res = await app.request("/setup", { redirect: "manual" });
133
+
134
+ expect(res.status).toBe(302);
135
+ expect(res.headers.get("Location")).toBe("/");
136
+ });
137
+ });
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Tests for the Discover answer the setup screen collects.
3
+ *
4
+ * The point under test is that setup records an answer rather than inferring
5
+ * one — a ticked box and a cleared box both write, and a form that carried no
6
+ * field at all writes nothing, since that last case is an older client or a
7
+ * scripted setup and reading it as a refusal would opt sites out that never
8
+ * said so — and then acts on it, on the same terms as the settings page's own
9
+ * switch.
10
+ */
11
+
12
+ import { describe, expect, it, vi } from "vitest";
13
+ import { Hono } from "hono";
14
+ import { createI18n } from "../../../i18n/index.js";
15
+ import { attachSession } from "../../../middleware/session.js";
16
+ import { ONBOARDING_STATUS } from "../../../lib/constants.js";
17
+ import { setupRoutes } from "../setup.js";
18
+ import type { Bindings } from "../../../types.js";
19
+ import type { AppVariables } from "../../../types/app-context.js";
20
+
21
+ type Env = { Bindings: Bindings; Variables: AppVariables };
22
+
23
+ function createMockAuth() {
24
+ return {
25
+ api: {
26
+ getSession: async () => ({
27
+ headers: new Headers(),
28
+ response: {
29
+ user: { id: "usr_owner", email: "owner@test.com", name: "O" },
30
+ session: { id: "ses_owner" },
31
+ },
32
+ }),
33
+ },
34
+ } as unknown as AppVariables["auth"];
35
+ }
36
+
37
+ /**
38
+ * A provisioned site whose owner is answering. Both install kinds reach the
39
+ * Discover write through this one branch — a self-hosted site arrives here from
40
+ * its second screen, a hosted one from its only screen.
41
+ */
42
+ function createSetupApp(
43
+ options: { demoMode?: boolean; shouldAnnounce?: boolean } = {},
44
+ ) {
45
+ const completeSiteSetup = vi.fn(async () => {});
46
+ const updateDiscoverSetting = vi.fn(async () => ({
47
+ shouldAnnounce: options.shouldAnnounce ?? true,
48
+ }));
49
+ const announceToDiscover = vi.fn(async () => {});
50
+ // The hook `runDeferred` offers tests: every backgrounded promise lands
51
+ // here, so the announcement can be awaited instead of raced.
52
+ const pending: Promise<unknown>[] = [];
53
+
54
+ const app = new Hono<Env>();
55
+ app.use("*", async (c, next) => {
56
+ c.env = {
57
+ DISCOVER_PING_URL: "https://directory.test/api/discover/ping",
58
+ __pendingDeferred: pending,
59
+ } as unknown as Bindings;
60
+ c.set("auth", createMockAuth());
61
+ c.set("services", {
62
+ settings: {
63
+ getOnboardingStatus: async () => ONBOARDING_STATUS.PROVISIONED,
64
+ // Already named, so this is the hosted screen: language and Discover
65
+ // and nothing else.
66
+ get: async () => "My Blog",
67
+ updateDiscoverSetting,
68
+ announceToDiscover,
69
+ },
70
+ bootstrap: { completeSiteSetup },
71
+ siteMembers: {
72
+ get: async () => ({
73
+ createdAt: 0,
74
+ role: "owner",
75
+ siteId: "sit_test",
76
+ updatedAt: 0,
77
+ userId: "usr_owner",
78
+ }),
79
+ },
80
+ } as unknown as AppVariables["services"]);
81
+ c.set("currentSite", {
82
+ createdAt: 0,
83
+ id: "sit_test",
84
+ key: "default",
85
+ status: "active",
86
+ updatedAt: 0,
87
+ });
88
+ c.set("appConfig", {
89
+ siteName: "Jant",
90
+ sitePathPrefix: "",
91
+ siteLanguage: "en",
92
+ siteUrl: "https://example.com",
93
+ demoMode: options.demoMode ?? false,
94
+ noindex: false,
95
+ rssFeedsEnabled: true,
96
+ } as AppVariables["appConfig"]);
97
+ c.set("lang", "en");
98
+ c.set("i18n", createI18n("en"));
99
+ await next();
100
+ });
101
+ app.use("*", attachSession());
102
+ app.route("/", setupRoutes);
103
+
104
+ return { app, updateDiscoverSetting, announceToDiscover, pending };
105
+ }
106
+
107
+ function post(app: Hono<Env>, body: Record<string, unknown>) {
108
+ return app.request("/setup", {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({ contentLanguage: "zh-Hans", ...body }),
112
+ });
113
+ }
114
+
115
+ describe("POST /setup, the Discover answer", () => {
116
+ it("stores `latest` when the box was ticked", async () => {
117
+ const { app, updateDiscoverSetting } = createSetupApp();
118
+
119
+ await post(app, { discover: true });
120
+
121
+ expect(updateDiscoverSetting).toHaveBeenCalledWith("latest", {
122
+ demoMode: false,
123
+ });
124
+ });
125
+
126
+ // The half that matters on a deployment which lists by default: a cleared box
127
+ // has to be written, or `DISCOVER=latest` keeps deciding for someone who just
128
+ // said no.
129
+ it("stores `off` when the box was cleared", async () => {
130
+ const { app, updateDiscoverSetting } = createSetupApp();
131
+
132
+ await post(app, { discover: false });
133
+
134
+ expect(updateDiscoverSetting).toHaveBeenCalledWith("off", {
135
+ demoMode: false,
136
+ });
137
+ });
138
+
139
+ it("writes nothing when the form carried no answer", async () => {
140
+ const { app, updateDiscoverSetting } = createSetupApp();
141
+
142
+ await post(app, {});
143
+
144
+ expect(updateDiscoverSetting).not.toHaveBeenCalled();
145
+ });
146
+
147
+ // The answer takes effect now, not when the author next happens to open a
148
+ // settings page they have no reason to open. A directory decides eligibility
149
+ // for itself and re-reads the feed on its own schedule, so a site with
150
+ // nothing published yet loses nothing by saying hello early.
151
+ it("announces the site when the box was ticked", async () => {
152
+ const { app, announceToDiscover, pending } = createSetupApp();
153
+
154
+ await post(app, { discover: true });
155
+ await Promise.all(pending);
156
+
157
+ expect(announceToDiscover).toHaveBeenCalledWith({
158
+ endpoint: "https://directory.test/api/discover/ping",
159
+ feedUrl: "https://example.com/latest/feed",
160
+ });
161
+ });
162
+
163
+ // Not awaited: a directory takes up to twelve seconds to give up on, and the
164
+ // last screen of setup is not a place to spend them. The outcome is recorded
165
+ // for the settings page's status block to read back.
166
+ it("does not hold the response while the directory answers", async () => {
167
+ const { app, announceToDiscover, pending } = createSetupApp();
168
+ let settled = false;
169
+ announceToDiscover.mockImplementation(async () => {
170
+ await new Promise((resolve) => setTimeout(resolve, 20));
171
+ settled = true;
172
+ });
173
+
174
+ await post(app, { discover: true });
175
+
176
+ expect(settled).toBe(false);
177
+ await Promise.all(pending);
178
+ expect(settled).toBe(true);
179
+ });
180
+
181
+ // Nothing to tell a directory that was never told anything: going from unset
182
+ // to `off` leaves the site exactly as unlisted as it already was.
183
+ it("announces nothing when the setting service reports no change", async () => {
184
+ const { app, announceToDiscover, pending } = createSetupApp({
185
+ shouldAnnounce: false,
186
+ });
187
+
188
+ await post(app, { discover: false });
189
+ await Promise.all(pending);
190
+
191
+ expect(announceToDiscover).not.toHaveBeenCalled();
192
+ });
193
+
194
+ it("announces nothing from a demo site", async () => {
195
+ const { app, announceToDiscover, pending } = createSetupApp({
196
+ demoMode: true,
197
+ });
198
+
199
+ await post(app, { discover: true });
200
+ await Promise.all(pending);
201
+
202
+ expect(announceToDiscover).not.toHaveBeenCalled();
203
+ });
204
+ });
@@ -0,0 +1,270 @@
1
+ /**
2
+ * The whole self-hosted setup, walked once with nothing mocked.
3
+ *
4
+ * Real services, a real better-auth, a real database: the other setup tests
5
+ * each hold one seam still, and this one is here to catch what only shows up
6
+ * when they are all moving — the session the first screen mints being the one
7
+ * the second screen accepts, the status the first screen writes being the one
8
+ * the second screen reads, and the site ending up named after neither of them
9
+ * guessed.
10
+ */
11
+
12
+ import { describe, expect, it, vi } from "vitest";
13
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
14
+ import { createAuth } from "../../../auth.js";
15
+ import { attachSession } from "../../../middleware/session.js";
16
+ import {
17
+ settings as settingsTable,
18
+ siteMembers,
19
+ user,
20
+ } from "../../../db/schema.js";
21
+ import { setupRoutes } from "../setup.js";
22
+
23
+ const PASSWORD = "correct-horse-battery";
24
+
25
+ function createSetupApp(options: Parameters<typeof createTestApp>[0] = {}) {
26
+ const { app, services, db } = createTestApp(options);
27
+
28
+ // The helper installs a stub that answers every session question the same
29
+ // way. Setup needs the real thing: it mints a session on the first screen and
30
+ // has to be believed on the second.
31
+ const auth = createAuth(db, {
32
+ secret: "test-secret-with-enough-entropy-for-session-cookies",
33
+ baseURL: "http://localhost",
34
+ useSecureCookies: false,
35
+ });
36
+ app.use("*", async (c, next) => {
37
+ c.set("auth", auth);
38
+ await next();
39
+ });
40
+ app.use("*", attachSession());
41
+ app.route("/", setupRoutes);
42
+
43
+ return { app, services, db, auth };
44
+ }
45
+
46
+ function post(
47
+ app: ReturnType<typeof createSetupApp>["app"],
48
+ body: Record<string, unknown>,
49
+ cookie?: string,
50
+ ) {
51
+ return app.request("/setup", {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ ...(cookie ? { Cookie: cookie } : {}),
56
+ },
57
+ body: JSON.stringify(body),
58
+ });
59
+ }
60
+
61
+ /** The session cookie a response minted, in the form a request sends back. */
62
+ function sessionCookie(res: Response): string {
63
+ const cookie = res.headers
64
+ .getSetCookie()
65
+ .find((value) => value.includes("session_token="));
66
+ expect(cookie).toBeDefined();
67
+ return cookie!.split(";")[0]!;
68
+ }
69
+
70
+ async function readSettings(db: ReturnType<typeof createSetupApp>["db"]) {
71
+ const rows = await db.select().from(settingsTable);
72
+ return Object.fromEntries(rows.map((row) => [row.key, row.value]));
73
+ }
74
+
75
+ describe("self-hosted setup, both screens", () => {
76
+ it("walks an empty install to a finished site", async () => {
77
+ const { app, db } = createSetupApp();
78
+
79
+ const first = await app.request("/setup");
80
+ await expect(first.text()).resolves.toContain("Setup · Step 1 of 2");
81
+
82
+ const account = await post(app, {
83
+ email: "owner@example.com",
84
+ password: PASSWORD,
85
+ });
86
+ const cookie = sessionCookie(account);
87
+
88
+ // Between the screens: an owner, a navigation profile, and no answers.
89
+ const midway = await readSettings(db);
90
+ expect(midway["ONBOARDING_STATUS"]).toBe("provisioned");
91
+ expect(midway["SITE_NAME"]).toBeUndefined();
92
+
93
+ // The session the first screen minted is the one the second screen trusts.
94
+ const second = await app.request("/setup", { headers: { Cookie: cookie } });
95
+ const secondHtml = await second.text();
96
+ expect(secondHtml).toContain("Setup · Step 2 of 2");
97
+ expect(secondHtml).toContain("setup-site-name");
98
+
99
+ await post(
100
+ app,
101
+ {
102
+ siteName: "My Blog",
103
+ contentLanguage: "zh-Hans",
104
+ language: "en-GB",
105
+ timezone: "Asia/Shanghai",
106
+ discover: true,
107
+ },
108
+ cookie,
109
+ );
110
+
111
+ const finished = await readSettings(db);
112
+ expect(finished["ONBOARDING_STATUS"]).toBe("completed");
113
+ expect(finished["SITE_NAME"]).toBe("My Blog");
114
+ expect(finished["SITE_LANGUAGE"]).toBe("zh-Hans");
115
+ expect(finished["TIME_ZONE"]).toBe("Asia/Shanghai");
116
+ expect(finished["DISCOVER"]).toBe("latest");
117
+ // Left following the content language. An English browser is what every
118
+ // unconfigured machine reports, so it is no evidence against the language
119
+ // the author just chose by hand one field above.
120
+ expect(finished["DASHBOARD_LANGUAGE"]).toBeUndefined();
121
+
122
+ // The account opened under the address, and the second screen renamed it.
123
+ const [owner] = await db.select().from(user);
124
+ expect(owner?.name).toBe("My Blog");
125
+ expect(owner?.email).toBe("owner@example.com");
126
+ });
127
+
128
+ // A finished site must never offer the screen that opens the first account
129
+ // again, to anyone, signed in or not.
130
+ it("closes setup once the site is finished", async () => {
131
+ const { app } = createSetupApp();
132
+
133
+ const cookie = sessionCookie(
134
+ await post(app, { email: "owner@example.com", password: PASSWORD }),
135
+ );
136
+ await post(app, { siteName: "My Blog", contentLanguage: "en" }, cookie);
137
+
138
+ const page = await app.request("/setup", {
139
+ headers: { Cookie: cookie },
140
+ redirect: "manual",
141
+ });
142
+ expect(page.status).toBe(302);
143
+ expect(page.headers.get("Location")).toBe("/");
144
+
145
+ const submitted = await post(
146
+ app,
147
+ { email: "someone@example.com", password: PASSWORD },
148
+ cookie,
149
+ );
150
+ expect(submitted.status).toBe(302);
151
+ });
152
+
153
+ // Losing the session between the screens is the failure the split invented.
154
+ // The site is provisioned, so the setup screen refuses an unauthenticated
155
+ // visitor — and `/signin` stays open for them, which is how they get back.
156
+ it("hides the second screen from a lost session", async () => {
157
+ const { app } = createSetupApp();
158
+
159
+ await post(app, { email: "owner@example.com", password: PASSWORD });
160
+
161
+ const res = await app.request("/setup", { redirect: "manual" });
162
+
163
+ expect(res.status).toBe(302);
164
+ expect(res.headers.get("Location")).toBe("/");
165
+ });
166
+
167
+ // The failure that used to brick an install: the account is opened, standing
168
+ // the site up then fails, and the site is left `pending` with a user row in
169
+ // it. From there `createAuth` refuses every signup and the onboarding
170
+ // middleware sends `/signin` and `/reset` back to this screen — so retrying
171
+ // the screen has to be the way out.
172
+ it("resumes a first screen that failed after opening the account", async () => {
173
+ const { app, services, db } = createSetupApp();
174
+ const provision = vi.spyOn(services.bootstrap, "provisionOwnerAccount");
175
+ provision.mockRejectedValueOnce(new Error("D1 unavailable"));
176
+
177
+ const failed = await post(app, {
178
+ email: "owner@example.com",
179
+ password: PASSWORD,
180
+ });
181
+ await expect(failed.text()).resolves.toContain("Couldn't create your");
182
+
183
+ // The account outlived the failure, and the site did not advance.
184
+ expect((await db.select().from(user)).length).toBe(1);
185
+ expect(await services.settings.getOnboardingStatus()).toBe("pending");
186
+
187
+ // Registration is closed to everyone now, this address included — which is
188
+ // why the retry has to resume rather than create.
189
+ const resumed = await post(app, {
190
+ email: "owner@example.com",
191
+ password: PASSWORD,
192
+ });
193
+
194
+ expect(await services.settings.getOnboardingStatus()).toBe("provisioned");
195
+ const second = await app.request("/setup", {
196
+ headers: { Cookie: sessionCookie(resumed) },
197
+ });
198
+ await expect(second.text()).resolves.toContain("Setup · Step 2 of 2");
199
+ // Still one account: resuming picked the existing one up.
200
+ expect((await db.select().from(user)).length).toBe(1);
201
+ });
202
+
203
+ it("refuses to resume on the wrong password", async () => {
204
+ const { app, services, db } = createSetupApp();
205
+ vi.spyOn(services.bootstrap, "provisionOwnerAccount").mockRejectedValueOnce(
206
+ new Error("D1 unavailable"),
207
+ );
208
+
209
+ await post(app, { email: "owner@example.com", password: PASSWORD });
210
+ const res = await post(app, {
211
+ email: "owner@example.com",
212
+ password: "not-the-password",
213
+ });
214
+
215
+ await expect(res.text()).resolves.toContain("Couldn't create your");
216
+ expect(res.headers.getSetCookie()).toHaveLength(0);
217
+ expect(await services.settings.getOnboardingStatus()).toBe("pending");
218
+ });
219
+
220
+ // The reason a different address is not an escape hatch, and so the reason
221
+ // resuming had to be built: the registration hook counts rows, not emails.
222
+ it("will not open a second account to get around it", async () => {
223
+ const { app, services, db } = createSetupApp();
224
+ vi.spyOn(services.bootstrap, "provisionOwnerAccount").mockRejectedValueOnce(
225
+ new Error("D1 unavailable"),
226
+ );
227
+
228
+ await post(app, { email: "owner@example.com", password: PASSWORD });
229
+ const res = await post(app, {
230
+ email: "someone-else@example.com",
231
+ password: PASSWORD,
232
+ });
233
+
234
+ await expect(res.text()).resolves.toContain("Couldn't create your");
235
+ expect((await db.select().from(user)).length).toBe(1);
236
+ expect(await services.settings.getOnboardingStatus()).toBe("pending");
237
+ });
238
+ });
239
+
240
+ // A hosted site's owner arrives through the control plane's handoff, never
241
+ // through this screen. The control plane creates each hosted site already
242
+ // provisioned, so a `pending` one is a site that lost its status somehow — and
243
+ // the database behind it holds every tenant's accounts. Signing in here with
244
+ // any of them must not come away owning the site.
245
+ describe("setup on a host-based install", () => {
246
+ it("never makes an existing account the owner of a hosted site", async () => {
247
+ const { app, services, db, auth } = createSetupApp({
248
+ siteResolutionMode: "host-based",
249
+ });
250
+ // Another tenant's owner. Being the first account, it also closes
251
+ // registration, as it is closed on any hosted database.
252
+ await auth.api.signUpEmail({
253
+ body: {
254
+ name: "Other Tenant",
255
+ email: "other@example.com",
256
+ password: PASSWORD,
257
+ },
258
+ });
259
+
260
+ const res = await post(app, {
261
+ email: "other@example.com",
262
+ password: PASSWORD,
263
+ });
264
+
265
+ await expect(res.text()).resolves.toContain("Couldn't create your");
266
+ expect(res.headers.getSetCookie()).toHaveLength(0);
267
+ expect(await db.select().from(siteMembers)).toHaveLength(0);
268
+ expect(await services.settings.getOnboardingStatus()).toBe("pending");
269
+ });
270
+ });