@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
@@ -1,26 +1,72 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { createTestDatabase } from "../../../__tests__/helpers/db.js";
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import {
4
+ createTestDatabase,
5
+ DEFAULT_TEST_SITE_ID,
6
+ } from "../../../__tests__/helpers/db.js";
3
7
  import { sql } from "drizzle-orm";
4
- import { navItems, settings, siteDomains, sites } from "../../../db/schema.js";
8
+ import {
9
+ navItems,
10
+ settings,
11
+ siteDomains,
12
+ siteMembers,
13
+ sites,
14
+ } from "../../../db/schema.js";
5
15
  import { createBootstrapService } from "../../../services/bootstrap.js";
16
+ import {
17
+ createSiteService,
18
+ TRANSIENT_SINGLE_SITE_ID,
19
+ } from "../../../services/site.js";
6
20
  import type { Database } from "../../../db/index.js";
7
21
  import type { BootstrapService } from "../../../services/bootstrap.js";
8
22
 
9
23
  /**
10
- * Reproduces the shell bootstrap logic from POST /setup to verify
11
- * setup stays idempotent even when managed shell data already exists.
24
+ * The bootstrap service one self-hosted setup request gets: bound to the site
25
+ * that request resolved, the way the runtime binds it. Resolved afresh for
26
+ * each screen, because the account step is what creates the site the second
27
+ * screen finds.
28
+ */
29
+ async function bootstrapForRequest(db: Database): Promise<BootstrapService> {
30
+ const { site } = await createSiteService(db).resolveSingleSite();
31
+ return createBootstrapService(db, site.id);
32
+ }
33
+
34
+ /**
35
+ * Reproduces both halves of POST /setup back to back — the shape a self-hosted
36
+ * install ends up in — to verify setup stays idempotent even when managed shell
37
+ * data already exists.
12
38
  */
13
39
  async function runSetupBootstrap(
14
- services: { bootstrap: BootstrapService },
15
- overrides: Partial<
16
- Parameters<BootstrapService["completeInitialSetup"]>[0]
17
- > = {},
40
+ services: { db: Database },
41
+ overrides: Partial<Parameters<BootstrapService["completeSiteSetup"]>[0]> = {},
18
42
  ) {
19
- await services.bootstrap.completeInitialSetup({
43
+ await (
44
+ await bootstrapForRequest(services.db)
45
+ ).provisionOwnerAccount({
20
46
  ownerUserId: "usr_test-owner",
21
- siteName: "Jant Demo",
22
- ...overrides,
23
47
  });
48
+ await (
49
+ await bootstrapForRequest(services.db)
50
+ ).completeSiteSetup(
51
+ { siteName: "Jant Demo", ...overrides },
52
+ { oldLanguage: "" },
53
+ );
54
+ }
55
+
56
+ /** Adds a second tenant, which is what a hosted database always has. */
57
+ async function insertOtherSite(
58
+ db: Database,
59
+ id = "sit_other00000000000000000000",
60
+ ) {
61
+ const timestamp = Math.floor(Date.now() / 1000);
62
+ await db.insert(sites).values({
63
+ id,
64
+ key: "other",
65
+ status: "active",
66
+ createdAt: timestamp - 60,
67
+ updatedAt: timestamp - 60,
68
+ });
69
+ return id;
24
70
  }
25
71
 
26
72
  describe("Setup bootstrap logic", () => {
@@ -34,7 +80,8 @@ describe("Setup bootstrap logic", () => {
34
80
  const db = testDb.db as unknown as Database;
35
81
  services = {
36
82
  db,
37
- bootstrap: createBootstrapService(db),
83
+ // The site both screens resolve once its row exists.
84
+ bootstrap: createBootstrapService(db, DEFAULT_TEST_SITE_ID),
38
85
  };
39
86
  });
40
87
 
@@ -46,9 +93,9 @@ describe("Setup bootstrap logic", () => {
46
93
 
47
94
  expect(navItemsList.map((item) => item.systemKey)).toEqual([
48
95
  "featured",
49
- "collections",
50
96
  "archive",
51
- "rss",
97
+ "collections",
98
+ "subscribe",
52
99
  "settings",
53
100
  ]);
54
101
  });
@@ -61,35 +108,150 @@ describe("Setup bootstrap logic", () => {
61
108
  expect(onboardingRow?.value).toBe("completed");
62
109
  });
63
110
 
64
- it("stores the initial language, CJK font, and timezone during setup", async () => {
111
+ // The state the site rests in between the two setup screens. It has to be
112
+ // `provisioned` and not `pending`: that is what lets an author who lost the
113
+ // session in between sign back in and finish, rather than being bounced back
114
+ // to a form that would try to create their account a second time.
115
+ it("leaves the site provisioned and unnamed after the account step", async () => {
116
+ await services.bootstrap.provisionOwnerAccount({
117
+ ownerUserId: "usr_test-owner",
118
+ });
119
+
120
+ const rows = await services.db.select().from(settings);
121
+ expect(rows.find((row) => row.key === "ONBOARDING_STATUS")?.value).toBe(
122
+ "provisioned",
123
+ );
124
+ expect(rows.find((row) => row.key === "SITE_NAME")).toBeUndefined();
125
+
126
+ const navItemsList = await services.db.select().from(navItems);
127
+ expect(navItemsList).toHaveLength(5);
128
+ });
129
+
130
+ // The hosted path through the same method: the control plane named the site
131
+ // at creation, and the author answering the last screen must not blank it.
132
+ it("leaves an existing name alone when the last screen asked for none", async () => {
133
+ await runSetupBootstrap(services);
134
+ await services.bootstrap.completeSiteSetup(
135
+ { siteLanguage: "en" },
136
+ { oldLanguage: "en" },
137
+ );
138
+
139
+ const rows = await services.db.select().from(settings);
140
+ expect(rows.find((row) => row.key === "SITE_NAME")?.value).toBe(
141
+ "Jant Demo",
142
+ );
143
+ });
144
+
145
+ it("renames the operator's account to match the site", async () => {
146
+ const updateCurrentUserName = vi.fn(async () => undefined);
147
+
148
+ await services.bootstrap.provisionOwnerAccount({
149
+ ownerUserId: "usr_test-owner",
150
+ });
151
+ await services.bootstrap.completeSiteSetup(
152
+ { siteName: " Jant Demo " },
153
+ { oldLanguage: "" },
154
+ { updateCurrentUserName },
155
+ );
156
+
157
+ expect(updateCurrentUserName).toHaveBeenCalledWith("Jant Demo");
158
+ });
159
+
160
+ // The rename runs before onboarding closes, so a better-auth failure leaves
161
+ // the author on the screen they were already on instead of inside a finished
162
+ // site under a stale account name.
163
+ it("leaves setup open when the account rename fails", async () => {
164
+ await services.bootstrap.provisionOwnerAccount({
165
+ ownerUserId: "usr_test-owner",
166
+ });
167
+ await expect(
168
+ services.bootstrap.completeSiteSetup(
169
+ { siteName: "Jant Demo" },
170
+ { oldLanguage: "" },
171
+ {
172
+ updateCurrentUserName: async () => {
173
+ throw new Error("better-auth unavailable");
174
+ },
175
+ },
176
+ ),
177
+ ).rejects.toThrow("better-auth unavailable");
178
+
179
+ const rows = await services.db.select().from(settings);
180
+ expect(rows.find((row) => row.key === "ONBOARDING_STATUS")?.value).toBe(
181
+ "provisioned",
182
+ );
183
+ });
184
+
185
+ it("stores the chosen content language and timezone during setup", async () => {
65
186
  await runSetupBootstrap(services, {
66
187
  siteLanguage: "en",
67
- cjkSerifFont: "zh-Hans",
68
188
  timeZone: "Asia/Shanghai",
69
189
  });
70
190
 
71
191
  const rows = await services.db.select().from(settings);
72
192
  expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe("en");
73
- expect(rows.find((row) => row.key === "CJK_SERIF_FONT")?.value).toBe(
74
- "zh-Hans",
75
- );
76
193
  expect(rows.find((row) => row.key === "TIME_ZONE")?.value).toBe(
77
194
  "Asia/Shanghai",
78
195
  );
79
196
  });
80
197
 
81
- it("pins the dashboard language to the detected catalog locale", async () => {
198
+ it("pins the dashboard language when only the browser knows it", async () => {
199
+ // Someone running an English blog from a Chinese browser: English site,
200
+ // Chinese dashboard. Following the content language would miss this.
201
+ await runSetupBootstrap(services, {
202
+ siteLanguage: "en",
203
+ browserLanguage: "zh-TW",
204
+ });
205
+
206
+ const rows = await services.db.select().from(settings);
207
+ expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe("en");
208
+ expect(rows.find((row) => row.key === "DASHBOARD_LANGUAGE")?.value).toBe(
209
+ "zh-Hant",
210
+ );
211
+ });
212
+
213
+ it("leaves the dashboard following a content language the author chose", async () => {
214
+ // The reverse mismatch, and the common one: an English browser is what
215
+ // every unconfigured machine reports, so it is no evidence against the
216
+ // language just chosen by hand. The dashboard must not freeze to English.
217
+ await runSetupBootstrap(services, {
218
+ siteLanguage: "zh-Hans",
219
+ browserLanguage: "en",
220
+ });
221
+
222
+ const rows = await services.db.select().from(settings);
223
+ expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe(
224
+ "zh-Hans",
225
+ );
226
+ expect(
227
+ rows.find((row) => row.key === "DASHBOARD_LANGUAGE"),
228
+ ).toBeUndefined();
229
+ });
230
+
231
+ it("leaves the dashboard following when the browser sent none", async () => {
82
232
  await runSetupBootstrap(services, { siteLanguage: "zh-TW" });
83
233
 
84
234
  const rows = await services.db.select().from(settings);
85
- // Content language stays verbatim; the dashboard locale is the resolved
86
- // catalog (zh-Hant) so it is stable if content language later changes.
235
+ // Content language stays verbatim, and the dashboard derives zh-Hant from
236
+ // it at render time rather than being pinned here.
87
237
  expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe(
88
238
  "zh-TW",
89
239
  );
90
- expect(rows.find((row) => row.key === "DASHBOARD_LANGUAGE")?.value).toBe(
91
- "zh-Hant",
92
- );
240
+ expect(
241
+ rows.find((row) => row.key === "DASHBOARD_LANGUAGE"),
242
+ ).toBeUndefined();
243
+ });
244
+
245
+ it("leaves the dashboard following when browser and content agree", async () => {
246
+ await runSetupBootstrap(services, {
247
+ siteLanguage: "zh-Hans",
248
+ browserLanguage: "zh-CN",
249
+ });
250
+
251
+ const rows = await services.db.select().from(settings);
252
+ expect(
253
+ rows.find((row) => row.key === "DASHBOARD_LANGUAGE"),
254
+ ).toBeUndefined();
93
255
  });
94
256
 
95
257
  it("is idempotent when default navigation already exists", async () => {
@@ -141,5 +303,95 @@ describe("Setup bootstrap logic", () => {
141
303
  const domainRows = await services.db.select().from(siteDomains);
142
304
  expect(siteRows).toHaveLength(1);
143
305
  expect(domainRows).toHaveLength(0);
306
+
307
+ const rows = await services.db.select().from(settings);
308
+ expect(rows.find((row) => row.key === "SITE_NAME")?.siteId).toBe(
309
+ siteRows[0]?.id,
310
+ );
311
+ });
312
+
313
+ // Settings written under the placeholder would belong to no site at all.
314
+ it("refuses to finish a site the account step has not created", async () => {
315
+ await services.db.run(sql`DELETE FROM "site"`);
316
+
317
+ await expect(
318
+ createBootstrapService(
319
+ services.db,
320
+ TRANSIENT_SINGLE_SITE_ID,
321
+ ).completeSiteSetup({ siteName: "Jant Demo" }, { oldLanguage: "" }),
322
+ ).rejects.toThrow("completeSiteSetup needs an existing site");
323
+
324
+ expect(await services.db.select().from(settings)).toHaveLength(0);
325
+ });
326
+ });
327
+
328
+ // A hosted database holds every tenant, so "the only site" has no answer there.
329
+ // Setup's last screen must write to the site the request's host resolved — the
330
+ // one the route's membership check just read — and leave every other tenant
331
+ // alone. The account screen has no hosted use at all.
332
+ describe("Setup bootstrap on a host-based install", () => {
333
+ function createHostedServices() {
334
+ const { services, db } = createTestApp({
335
+ siteResolutionMode: "host-based",
336
+ });
337
+ return { services, db };
338
+ }
339
+
340
+ it("finishes the request's site while other sites share the database", async () => {
341
+ const { services, db } = createHostedServices();
342
+ const otherSiteId = await insertOtherSite(db);
343
+
344
+ await services.bootstrap.completeSiteSetup(
345
+ { siteLanguage: "en" },
346
+ { oldLanguage: "" },
347
+ );
348
+
349
+ const rows = await db.select().from(settings);
350
+ expect(
351
+ rows.find(
352
+ (row) =>
353
+ row.siteId === DEFAULT_TEST_SITE_ID &&
354
+ row.key === "ONBOARDING_STATUS",
355
+ )?.value,
356
+ ).toBe("completed");
357
+ expect(rows.filter((row) => row.siteId === otherSiteId)).toHaveLength(0);
358
+ });
359
+
360
+ // A hosted site's owner arrives through the control plane's handoff. The
361
+ // account step would make whoever signed in the owner of the site the host
362
+ // resolved, so it refuses before writing anything to any tenant.
363
+ it("refuses the account step and writes nothing", async () => {
364
+ const { services, db } = createHostedServices();
365
+ await insertOtherSite(db);
366
+
367
+ await expect(
368
+ services.bootstrap.provisionOwnerAccount({
369
+ ownerUserId: "usr_test-owner",
370
+ }),
371
+ ).rejects.toThrow(
372
+ "provisionOwnerAccount runs on self-hosted installs only",
373
+ );
374
+
375
+ expect(await db.select().from(sites)).toHaveLength(2);
376
+ expect(await db.select().from(siteMembers)).toHaveLength(0);
377
+ expect(await db.select().from(navItems)).toHaveLength(0);
378
+ expect(await db.select().from(settings)).toHaveLength(0);
379
+ });
380
+
381
+ // The control plane creates hosted sites. Materializing one here would add a
382
+ // stray tenant to a database that holds real ones.
383
+ it("never creates a site when the request resolved none", async () => {
384
+ const db = createTestDatabase().db as unknown as Database;
385
+ await db.run(sql`DELETE FROM "site"`);
386
+
387
+ await expect(
388
+ createBootstrapService(db, TRANSIENT_SINGLE_SITE_ID, {
389
+ siteResolutionMode: "host-based",
390
+ }).provisionOwnerAccount({ ownerUserId: "usr_test-owner" }),
391
+ ).rejects.toThrow(
392
+ "provisionOwnerAccount runs on self-hosted installs only",
393
+ );
394
+
395
+ expect(await db.select().from(sites)).toHaveLength(0);
144
396
  });
145
397
  });
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Hono } from "hono";
3
+ import { createI18n } from "../../../i18n/index.js";
4
+ import { attachSession } from "../../../middleware/session.js";
5
+ import { requireAuth } from "../../../middleware/auth.js";
6
+ import { signinRoutes } from "../signin.js";
7
+ import type { Bindings } from "../../../types.js";
8
+ import type { AppVariables } from "../../../types/app-context.js";
9
+
10
+ type Env = { Bindings: Bindings; Variables: AppVariables };
11
+
12
+ const SIGNIN_FORM_MARKER = 'data-bind="password"';
13
+
14
+ /**
15
+ * A session for `usr_stale` that the site knows nothing about — what a browser
16
+ * still holds right after a factory reset, since better-auth answers from its
17
+ * cookie cache for minutes after the rows are gone.
18
+ */
19
+ function createMockAuth(signedIn: boolean) {
20
+ return {
21
+ api: {
22
+ getSession: async () => ({
23
+ headers: new Headers(),
24
+ response: signedIn
25
+ ? {
26
+ user: { id: "usr_stale", email: "old@test.com", name: "Old" },
27
+ session: { id: "ses_stale" },
28
+ }
29
+ : null,
30
+ }),
31
+ },
32
+ } as unknown as AppVariables["auth"];
33
+ }
34
+
35
+ function createMockServices(isMember: boolean) {
36
+ return {
37
+ siteMembers: {
38
+ get: async () =>
39
+ isMember
40
+ ? {
41
+ createdAt: 0,
42
+ role: "owner",
43
+ siteId: "sit_test",
44
+ updatedAt: 0,
45
+ userId: "usr_stale",
46
+ }
47
+ : null,
48
+ },
49
+ } as unknown as AppVariables["services"];
50
+ }
51
+
52
+ function createSigninTestApp(options: {
53
+ signedIn: boolean;
54
+ isMember: boolean;
55
+ }) {
56
+ const app = new Hono<Env>();
57
+ app.use("*", async (c, next) => {
58
+ c.env = {} as Bindings;
59
+ c.set("auth", createMockAuth(options.signedIn));
60
+ c.set("services", createMockServices(options.isMember));
61
+ c.set("currentSite", {
62
+ createdAt: 0,
63
+ id: "sit_test",
64
+ key: "default",
65
+ status: "active",
66
+ updatedAt: 0,
67
+ });
68
+ c.set("currentSiteDomain", null);
69
+ c.set("appConfig", {
70
+ siteName: "Jant",
71
+ sitePathPrefix: "",
72
+ } as AppVariables["appConfig"]);
73
+ c.set("publicRequestUrl", "http://localhost/signin");
74
+ c.set("lang", "en");
75
+ c.set("i18n", createI18n("en"));
76
+ await next();
77
+ });
78
+ app.use("*", attachSession());
79
+ app.route("/", signinRoutes);
80
+ app.use("/settings", requireAuth());
81
+ app.get("/settings", (c) => c.text("Settings"));
82
+ return app;
83
+ }
84
+
85
+ describe("GET /signin", () => {
86
+ it("shows the form to a session that is not a member of this site", async () => {
87
+ const app = createSigninTestApp({ signedIn: true, isMember: false });
88
+
89
+ const res = await app.request("/signin?redirect=%2Fsettings", {
90
+ redirect: "manual",
91
+ });
92
+
93
+ expect(res.status).toBe(200);
94
+ expect(await res.text()).toContain(SIGNIN_FORM_MARKER);
95
+ });
96
+
97
+ it("sends a member on to where they were headed", async () => {
98
+ const app = createSigninTestApp({ signedIn: true, isMember: true });
99
+
100
+ const res = await app.request("/signin?redirect=%2Fsettings", {
101
+ redirect: "manual",
102
+ });
103
+
104
+ expect(res.status).toBe(302);
105
+ expect(res.headers.get("Location")).toBe("/settings");
106
+ });
107
+
108
+ it("still shows the form to a visitor with no session", async () => {
109
+ const app = createSigninTestApp({ signedIn: false, isMember: false });
110
+
111
+ const res = await app.request("/signin", { redirect: "manual" });
112
+
113
+ expect(res.status).toBe(200);
114
+ expect(await res.text()).toContain(SIGNIN_FORM_MARKER);
115
+ });
116
+ });
117
+
118
+ describe("stale session redirect loop", () => {
119
+ it("lands on the sign-in form instead of volleying with /settings", async () => {
120
+ const app = createSigninTestApp({ signedIn: true, isMember: false });
121
+
122
+ // `requireAuth` requires membership, so a session left over from a deleted
123
+ // site is turned away here.
124
+ const guarded = await app.request("/settings", { redirect: "manual" });
125
+ expect(guarded.status).toBe(302);
126
+ const signinLocation = guarded.headers.get("Location");
127
+ expect(signinLocation).toBe("/signin?redirect=%2Fsettings");
128
+
129
+ // …and this is the hop that used to send the same session straight back to
130
+ // /settings, three redirects per lap until the browser gave up.
131
+ const signin = await app.request(signinLocation!, { redirect: "manual" });
132
+ expect(signin.status).toBe(200);
133
+ expect(await signin.text()).toContain(SIGNIN_FORM_MARKER);
134
+ });
135
+ });