@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
@@ -21,8 +21,12 @@ import {
21
21
  import type { SettingsService } from "./settings.js";
22
22
  import type { StorageDriver } from "../lib/storage.js";
23
23
  import { SETTINGS_KEYS } from "../lib/constants.js";
24
- import { timingSafeEqualText } from "../lib/crypto.js";
25
- import { ValidationError, NotFoundError } from "../lib/errors.js";
24
+ import { hmacHex, timingSafeEqualText } from "../lib/crypto.js";
25
+ import {
26
+ ConfigurationError,
27
+ ValidationError,
28
+ NotFoundError,
29
+ } from "../lib/errors.js";
26
30
  import { hashPassword } from "../lib/password.js";
27
31
 
28
32
  /** Dependencies for account deletion */
@@ -53,20 +57,27 @@ export interface AuthService {
53
57
  resetPassword(token: string, newPassword: string): Promise<void>;
54
58
 
55
59
  /**
56
- * Generate a short-lived CSRF token for account deletion.
57
- * Stores the hash + expiry in settings and returns the plaintext.
60
+ * Derive the account-deletion CSRF token for a session.
61
+ *
62
+ * Derived, not stored: rendering the delete page is a GET, and a GET that
63
+ * wrote a fresh token somewhere would invalidate every copy handed out
64
+ * before it — a second tab, a prefetch, or a browser extension re-fetching
65
+ * the page would silently break the button the author is looking at. Same
66
+ * session in, same token out, however many times it is asked for.
58
67
  *
59
- * @returns The plaintext CSRF token to embed in the page
68
+ * @param sessionId - The signed-in session this token is bound to
69
+ * @returns The token to embed in the page
60
70
  */
61
- generateDeleteCsrfToken(): Promise<string>;
71
+ generateDeleteCsrfToken(sessionId: string): Promise<string>;
62
72
 
63
73
  /**
64
- * Validate a CSRF token for account deletion.
74
+ * Validate an account-deletion CSRF token against the requesting session.
65
75
  *
66
- * @param token - The plaintext CSRF token from the form
67
- * @returns true if valid and not expired
76
+ * @param token - The token submitted with the request
77
+ * @param sessionId - The session the request arrived on
78
+ * @returns `true` when the token belongs to this session
68
79
  */
69
- validateDeleteCsrfToken(token: string): Promise<boolean>;
80
+ validateDeleteCsrfToken(token: string, sessionId: string): Promise<boolean>;
70
81
 
71
82
  /**
72
83
  * Delete all user data and reset the instance to a pristine state.
@@ -87,6 +98,8 @@ export function createAuthService(
87
98
  settings: SettingsService,
88
99
  config?: {
89
100
  databaseDialect?: DatabaseDialect;
101
+ /** Signing key for derived tokens. Absent only where no route needs one. */
102
+ authSecret?: string;
90
103
  },
91
104
  databaseSchema: DatabaseSchema = sqliteSchemaBundle,
92
105
  ): AuthService {
@@ -97,6 +110,7 @@ export function createAuthService(
97
110
  verification,
98
111
  media,
99
112
  collections,
113
+ smartCollections,
100
114
  threadCollections,
101
115
  pathRegistry,
102
116
  collectionDirectoryItems: directoryItemsTable,
@@ -108,6 +122,7 @@ export function createAuthService(
108
122
  apiTokens,
109
123
  } = databaseSchema;
110
124
  const databaseDialect = config?.databaseDialect ?? "sqlite";
125
+ const authSecret = config?.authSecret;
111
126
 
112
127
  async function deleteDataRows(targetDb: Database): Promise<void> {
113
128
  // Junction/dependent tables first
@@ -125,6 +140,12 @@ export function createAuthService(
125
140
  );
126
141
  await executeStatement(targetDb, sql`DELETE FROM post`);
127
142
 
143
+ // Both kinds of collection, and smart ones first — a smart collection holds
144
+ // a `collection_id`. There is no constraint forcing that order any more
145
+ // (see the note on the column in `db/schema.ts`), but a reset that walks
146
+ // every table by hand should not leave one of them to an implicit cascade
147
+ // from `site` at the very end.
148
+ await targetDb.delete(smartCollections);
128
149
  await targetDb.delete(collections);
129
150
  await targetDb.delete(apiTokens);
130
151
  await targetDb.delete(settingsTable);
@@ -174,14 +195,21 @@ export function createAuthService(
174
195
  return timingSafeEqualText(tokenHash, storedHash);
175
196
  }
176
197
 
177
- async function hashToken(token: string): Promise<string> {
178
- const hashBuffer = await crypto.subtle.digest(
179
- "SHA-256",
180
- new TextEncoder().encode(token),
181
- );
182
- return Array.from(new Uint8Array(hashBuffer))
183
- .map((b) => b.toString(16).padStart(2, "0"))
184
- .join("");
198
+ /**
199
+ * The delete-account token for a session — a pure function of the session and
200
+ * the server's signing key, so nothing has to be written down to check it
201
+ * later. It stops being valid when the session does, which is the only
202
+ * lifetime that means anything here: the token is a second factor on top of
203
+ * the session cookie, useless to anyone who does not already hold it.
204
+ */
205
+ async function deriveDeleteCsrfToken(sessionId: string): Promise<string> {
206
+ if (!authSecret) {
207
+ throw new ConfigurationError(
208
+ "AUTH_SECRET is required to protect account deletion.",
209
+ );
210
+ }
211
+
212
+ return hmacHex(authSecret, `delete-account:${sessionId}`);
185
213
  }
186
214
 
187
215
  return {
@@ -217,39 +245,13 @@ export function createAuthService(
217
245
  await settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
218
246
  },
219
247
 
220
- async generateDeleteCsrfToken() {
221
- const bytes = new Uint8Array(32);
222
- crypto.getRandomValues(bytes);
223
- const plaintext = Array.from(bytes)
224
- .map((b) => b.toString(16).padStart(2, "0"))
225
- .join("");
226
-
227
- const tokenHash = await hashToken(plaintext);
228
- // Token valid for 15 minutes
229
- const expiry = Math.floor(Date.now() / 1000) + 15 * 60;
230
-
231
- await settings.set(
232
- SETTINGS_KEYS.DELETE_CSRF_TOKEN,
233
- `${tokenHash}:${expiry}`,
234
- );
235
-
236
- return plaintext;
248
+ async generateDeleteCsrfToken(sessionId) {
249
+ return deriveDeleteCsrfToken(sessionId);
237
250
  },
238
251
 
239
- async validateDeleteCsrfToken(token) {
240
- const stored = await settings.get(SETTINGS_KEYS.DELETE_CSRF_TOKEN);
241
- if (!stored) return false;
242
-
243
- const separatorIndex = stored.lastIndexOf(":");
244
- const storedHash = stored.substring(0, separatorIndex);
245
- const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
246
- const now = Math.floor(Date.now() / 1000);
247
-
248
- if (now > expiry) return false;
249
-
250
- const tokenHash = await hashToken(token);
251
-
252
- return timingSafeEqualText(tokenHash, storedHash);
252
+ async validateDeleteCsrfToken(token, sessionId) {
253
+ const expected = await deriveDeleteCsrfToken(sessionId);
254
+ return timingSafeEqualText(token, expected);
253
255
  },
254
256
 
255
257
  async deleteAllData(deps) {
@@ -1,83 +1,233 @@
1
1
  /**
2
2
  * Bootstrap Service
3
3
  *
4
- * Owns first-run site shell setup after account creation.
4
+ * Owns first-run setup, which runs in two acts on a self-hosted site: the
5
+ * account is created first, and everything about the site itself is settled
6
+ * afterwards. The two are separate methods because the site is left standing
7
+ * between them — `markSiteProvisioned` closes the first act, which is the same
8
+ * state a control plane leaves a hosted site in, so the second act is one code
9
+ * path for both install kinds.
10
+ *
11
+ * The first act belongs to self-hosted installs alone. A hosted site gets its
12
+ * owner through the control plane's handoff, so on a host-based install only
13
+ * the second act runs, against the site the request resolved — the same site
14
+ * the route's membership check reads. On a hosted install the database holds
15
+ * every tenant, so "the only site" is not a question with an answer there.
5
16
  */
6
17
 
18
+ import type { DatabaseDialect } from "../db/dialect.js";
7
19
  import type { Database } from "../db/index.js";
8
20
  import {
9
21
  sqliteSchemaBundle,
10
22
  type DatabaseSchema,
11
23
  } from "../db/schema-bundle.js";
12
- import {
13
- baseLocale,
14
- isValidContentLanguage,
15
- normalizeContentLanguage,
16
- resolveCatalogLocale,
17
- } from "../i18n/locales.js";
18
24
  import { createNavItemService } from "./navigation.js";
19
25
  import { createSettingsService } from "./settings.js";
20
26
  import { createSiteMemberService } from "./site-member.js";
21
- import { createSiteService, type EnsureSingleSiteOptions } from "./site.js";
27
+ import {
28
+ createSiteService,
29
+ TRANSIENT_SINGLE_SITE_ID,
30
+ type EnsureSingleSiteOptions,
31
+ } from "./site.js";
22
32
 
23
- export interface CompleteInitialSetupData {
33
+ export interface ProvisionOwnerAccountData {
34
+ /** The account the first setup screen just created. */
24
35
  ownerUserId: string;
25
- siteName: string;
36
+ }
37
+
38
+ export interface CompleteSiteSetupData {
39
+ /**
40
+ * The site's name, on an install that does not have one yet.
41
+ *
42
+ * Omitted on a hosted site: the control plane named it at creation, so the
43
+ * second screen never asks and must not overwrite.
44
+ */
45
+ siteName?: string | null;
46
+ /** Language the site publishes in, chosen explicitly during setup. */
26
47
  siteLanguage?: string | null;
27
- cjkSerifFont?: string | null;
48
+ /**
49
+ * The browser's own language, used only to pin the dashboard UI locale.
50
+ *
51
+ * Kept separate from `siteLanguage` because the two genuinely differ for the
52
+ * people this matters to most: someone running an English-language blog from
53
+ * a Chinese browser wants an English site and a Chinese dashboard.
54
+ */
55
+ browserLanguage?: string | null;
56
+ /**
57
+ * The browser's time zone, already mapped to a supported one by the caller.
58
+ *
59
+ * Omitted where the shell already carries one, so a second screen loaded in
60
+ * a different place cannot move a hosted site's clock.
61
+ */
28
62
  timeZone?: string | null;
29
63
  }
30
64
 
65
+ export interface CompleteSiteSetupDeps {
66
+ /**
67
+ * Align the operator's better-auth account name with the site's display
68
+ * name, the way the general settings page does whenever the name changes.
69
+ *
70
+ * Injected rather than called here: the update needs the request's own
71
+ * headers to know whose account it is, and a service has none.
72
+ */
73
+ updateCurrentUserName?: ((displayName: string) => Promise<void>) | null;
74
+ }
75
+
31
76
  export interface BootstrapService {
32
77
  /**
33
- * Complete first-run setup for a newly created account.
34
- * Materializes the versioned navigation profile and marks onboarding complete
35
- * last so an interrupted setup can recover safely.
78
+ * Stand the site up around a newly created owner account.
79
+ *
80
+ * Everything here is what a site needs to exist at all — a row, an owner, a
81
+ * navigation profile. Nothing here is an answer the author gave, because at
82
+ * this point they have given none beyond their credentials.
83
+ *
84
+ * Ends by marking the site provisioned rather than complete. That is what
85
+ * makes the gap between the two screens survivable: at `provisioned` the
86
+ * onboarding middleware serves `/signin` normally, so an author who loses
87
+ * the session before finishing signs back in and lands on the second screen.
36
88
  *
37
- * @param data - Initial site shell values captured during setup
89
+ * @param data - The account the first screen created
90
+ * @throws {Error} On a host-based install, where a site's owner arrives
91
+ * through the control plane's handoff instead
38
92
  */
39
- completeInitialSetup(data: CompleteInitialSetupData): Promise<void>;
93
+ provisionOwnerAccount(data: ProvisionOwnerAccountData): Promise<void>;
94
+
95
+ /**
96
+ * Close first-run setup by recording what the author said about their site.
97
+ *
98
+ * Shared by both install kinds: a self-hosted site arrives here from its
99
+ * second screen with a name to set, a hosted site from its only screen with
100
+ * the name already stored. Onboarding is marked complete last, so an
101
+ * interrupted run resumes rather than stranding a half-answered site.
102
+ *
103
+ * @param data - The answers the second screen collected
104
+ * @param opts - The language in effect before this answer
105
+ * @param deps - Cross-cutting work the route owns, such as the better-auth
106
+ * account rename that needs the request's headers
107
+ */
108
+ completeSiteSetup(
109
+ data: CompleteSiteSetupData,
110
+ opts: { oldLanguage: string },
111
+ deps?: CompleteSiteSetupDeps,
112
+ ): Promise<void>;
40
113
  }
41
114
 
115
+ /**
116
+ * Create the first-run setup service for one request.
117
+ *
118
+ * @param db - The database
119
+ * @param siteId - The site the request resolved. On a self-hosted install that
120
+ * has not finished the account step this is the transient placeholder, since
121
+ * no site row exists yet.
122
+ * @param options - The schema, dialect and resolution mode the runtime uses,
123
+ * and where a self-hosted site's row and domain come from
124
+ * @returns The bootstrap service bound to that site
125
+ */
42
126
  export function createBootstrapService(
43
127
  db: Database,
128
+ siteId: string,
44
129
  options?: {
45
130
  schema?: DatabaseSchema;
131
+ databaseDialect?: DatabaseDialect;
46
132
  bootstrapSite?: EnsureSingleSiteOptions;
133
+ siteResolutionMode?: "single-site" | "host-based";
47
134
  },
48
135
  ): BootstrapService {
49
136
  const databaseSchema = options?.schema ?? sqliteSchemaBundle;
137
+ const dialect = options?.databaseDialect ?? "sqlite";
138
+ const siteResolutionMode = options?.siteResolutionMode ?? "single-site";
139
+
140
+ /**
141
+ * The request's site, which must be a real row by the time anything is
142
+ * written to it. The placeholder only stands in for a self-hosted site the
143
+ * account step has yet to create.
144
+ */
145
+ function existingSiteId(step: string): string {
146
+ if (siteId === TRANSIENT_SINGLE_SITE_ID) {
147
+ throw new Error(
148
+ `${step} needs an existing site, but the request resolved none.`,
149
+ );
150
+ }
151
+ return siteId;
152
+ }
153
+
154
+ /**
155
+ * The site the account step stands up. A self-hosted install has no row
156
+ * until now, so this is the step that creates it — along with its domain,
157
+ * when one is configured.
158
+ *
159
+ * A host-based install refuses before anything is written. The control
160
+ * plane creates each hosted site already provisioned, and its owner arrives
161
+ * through the hosted handoff, the only way anyone becomes a member of one.
162
+ * Attaching whoever just signed in to the site the host resolved would let
163
+ * any account in a database every tenant shares take a site that had lost
164
+ * its onboarding status.
165
+ */
166
+ async function siteToProvision(): Promise<string> {
167
+ if (siteResolutionMode === "host-based") {
168
+ throw new Error(
169
+ "provisionOwnerAccount runs on self-hosted installs only. A hosted site's owner arrives through the control plane's handoff.",
170
+ );
171
+ }
172
+ const { site } = await createSiteService(
173
+ db,
174
+ databaseSchema,
175
+ ).ensureSingleSite(options?.bootstrapSite);
176
+ return site.id;
177
+ }
50
178
 
51
179
  return {
52
- async completeInitialSetup(data) {
53
- const siteService = createSiteService(db, databaseSchema);
54
- const { site } = await siteService.ensureSingleSite(
55
- options?.bootstrapSite,
180
+ async provisionOwnerAccount(data) {
181
+ const provisionedSiteId = await siteToProvision();
182
+ const settings = createSettingsService(
183
+ db,
184
+ provisionedSiteId,
185
+ databaseSchema,
186
+ dialect,
187
+ );
188
+ const navItems = createNavItemService(
189
+ db,
190
+ provisionedSiteId,
191
+ databaseSchema,
56
192
  );
57
- const settings = createSettingsService(db, site.id, databaseSchema);
58
- const navItems = createNavItemService(db, site.id, databaseSchema);
59
193
  const siteMembers = createSiteMemberService(db, databaseSchema);
60
194
 
61
- await siteMembers.ensure(site.id, data.ownerUserId, "owner");
195
+ await siteMembers.ensure(provisionedSiteId, data.ownerUserId, "owner");
62
196
  await navItems.materializeDefaultNavigation();
63
- await settings.set("SITE_NAME", data.siteName.trim());
64
- await settings.set("TIME_ZONE", data.timeZone ?? "UTC");
65
- const siteLanguage =
66
- data.siteLanguage && isValidContentLanguage(data.siteLanguage)
67
- ? normalizeContentLanguage(data.siteLanguage)
68
- : baseLocale;
69
- await settings.set("SITE_LANGUAGE", siteLanguage);
70
- // Pin the dashboard UI locale to the detected catalog language so it stays
71
- // stable if the operator later changes the public content language.
72
- await settings.set(
73
- "DASHBOARD_LANGUAGE",
74
- resolveCatalogLocale(siteLanguage),
197
+ await settings.markSiteProvisioned();
198
+ },
199
+
200
+ async completeSiteSetup(data, opts, deps) {
201
+ const settings = createSettingsService(
202
+ db,
203
+ existingSiteId("completeSiteSetup"),
204
+ databaseSchema,
205
+ dialect,
75
206
  );
76
- if (data.cjkSerifFont && data.cjkSerifFont !== "off") {
77
- await settings.set("CJK_SERIF_FONT", data.cjkSerifFont);
207
+
208
+ const siteName = data.siteName?.trim();
209
+ if (siteName) {
210
+ await settings.set("SITE_NAME", siteName);
211
+ // Before onboarding closes, deliberately: if the rename fails the site
212
+ // is still provisioned, and the author retries the screen they were
213
+ // already on rather than landing in a finished site under a stale name.
214
+ await deps?.updateCurrentUserName?.(siteName);
78
215
  }
79
216
 
80
- await settings.completeOnboarding();
217
+ // `undefined` means the shell already has one. `null` is a browser that
218
+ // reported nothing, which is the same UTC default the site would fall
219
+ // back to anyway — written so a later reader sees a chosen value.
220
+ if (data.timeZone !== undefined) {
221
+ await settings.set("TIME_ZONE", data.timeZone ?? "UTC");
222
+ }
223
+
224
+ await settings.confirmFirstRunLanguage(
225
+ {
226
+ siteLanguage: data.siteLanguage ?? "",
227
+ browserLanguage: data.browserLanguage,
228
+ },
229
+ opts,
230
+ );
81
231
  },
82
232
  };
83
233
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Positions in the collections directory.
3
+ *
4
+ * The directory is one ordered list — manual collections, smart collections,
5
+ * dividers, and links share it — ordered by a fractional index, so a move
6
+ * rewrites a single row instead of renumbering the list.
7
+ *
8
+ * Two services write into that list: `collection` places manual collections
9
+ * along with the author's dividers and links, `smart-collection` places smart
10
+ * collections. The append rule lives here so the second writer cannot invent a
11
+ * different one.
12
+ */
13
+
14
+ import { eq, sql } from "drizzle-orm";
15
+ import { generateKeyBetween } from "fractional-indexing";
16
+ import type { Database } from "../db/index.js";
17
+ import type { DatabaseSchema } from "../db/schema-bundle.js";
18
+
19
+ type DirectoryItemsTable = DatabaseSchema["collectionDirectoryItems"];
20
+
21
+ /**
22
+ * How many times a writer retries after two rows land on the same position.
23
+ *
24
+ * `(site_id, position)` is unique, so a concurrent append can lose the race;
25
+ * the next attempt reads the new last position and appends after it.
26
+ */
27
+ export const DIRECTORY_POSITION_RETRY_ATTEMPTS = 5;
28
+
29
+ /**
30
+ * The position of the last row in a site's collections directory.
31
+ *
32
+ * @param db - Database handle
33
+ * @param table - The `collection_directory_item` table for this dialect
34
+ * @param siteId - Site the directory belongs to
35
+ * @returns The largest position, or `null` when the directory is empty
36
+ * @example
37
+ * ```ts
38
+ * const last = await getLastDirectoryPosition(db, directoryItems, siteId);
39
+ * ```
40
+ */
41
+ export async function getLastDirectoryPosition(
42
+ db: Database,
43
+ table: DirectoryItemsTable,
44
+ siteId: string,
45
+ ): Promise<string | null> {
46
+ const rows = await db
47
+ .select({ position: table.position })
48
+ .from(table)
49
+ .where(eq(table.siteId, siteId))
50
+ .orderBy(sql`${table.position} DESC`)
51
+ .limit(1);
52
+ return rows[0]?.position ?? null;
53
+ }
54
+
55
+ /**
56
+ * A position that puts a new row at the end of the directory.
57
+ *
58
+ * @param db - Database handle
59
+ * @param table - The `collection_directory_item` table for this dialect
60
+ * @param siteId - Site the directory belongs to
61
+ * @returns A fractional index greater than every existing position
62
+ * @example
63
+ * ```ts
64
+ * const position = await getAppendDirectoryPosition(db, directoryItems, siteId);
65
+ * ```
66
+ */
67
+ export async function getAppendDirectoryPosition(
68
+ db: Database,
69
+ table: DirectoryItemsTable,
70
+ siteId: string,
71
+ ): Promise<string> {
72
+ return generateKeyBetween(
73
+ await getLastDirectoryPosition(db, table, siteId),
74
+ null,
75
+ );
76
+ }