@jant/core 0.6.16 → 0.7.1

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 (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -5,10 +5,15 @@ import {
5
5
  } from "../../__tests__/helpers/db.js";
6
6
  import { sqliteSchemaBundle } from "../../db/schema-bundle.js";
7
7
  import { createRequestRuntime } from "../index.js";
8
- import { createNodeRequestRuntime } from "../node.js";
8
+ import { createNodeCliRuntime, createNodeRequestRuntime } from "../node.js";
9
+ import type { CliSiteSelector } from "../site.js";
9
10
  import type { Bindings } from "../../types.js";
10
11
  import { siteDomains, sites } from "../../db/schema.js";
11
- import { ConflictError, NotFoundError } from "../../lib/errors.js";
12
+ import {
13
+ ConflictError,
14
+ NotFoundError,
15
+ SiteUnavailableError,
16
+ } from "../../lib/errors.js";
12
17
  import { TRANSIENT_SINGLE_SITE_ID } from "../../services/site.js";
13
18
 
14
19
  afterEach(() => {
@@ -217,7 +222,7 @@ describe("createNodeRequestRuntime", () => {
217
222
  );
218
223
  });
219
224
 
220
- it("treats suspended host-based sites as not found", async () => {
225
+ it("treats suspended host-based sites as unavailable, not missing", async () => {
221
226
  const { db, sqlite } = createTestDatabase();
222
227
  const consoleError = vi
223
228
  .spyOn(console, "error")
@@ -250,9 +255,129 @@ describe("createNodeRequestRuntime", () => {
250
255
  } as Bindings,
251
256
  "http://suspended.localtest.me/",
252
257
  ),
253
- ).rejects.toBeInstanceOf(NotFoundError);
258
+ ).rejects.toBeInstanceOf(SiteUnavailableError);
254
259
  expect(consoleError).toHaveBeenCalledWith(
255
260
  "[Jant] Hosted site resolution failed: host=suspended.localtest.me path=/ reason=site-not-active siteId=sit_test00000000000000000000000 siteKey=default siteStatus=suspended",
256
261
  );
257
262
  });
258
263
  });
264
+
265
+ describe("createNodeCliRuntime", () => {
266
+ const OTHER_SITE_ID = "sit_other0000000000000000000000";
267
+
268
+ function nodeDatabaseBindings(
269
+ testDb: ReturnType<typeof createTestDatabase>,
270
+ env: Partial<Bindings> = {},
271
+ ): Bindings {
272
+ return {
273
+ NODE_DATABASE: {
274
+ db: testDb.db,
275
+ dialect: "sqlite",
276
+ rawQuery: createSqliteRawQuery(testDb.sqlite),
277
+ schema: sqliteSchemaBundle,
278
+ },
279
+ ...env,
280
+ } as Bindings;
281
+ }
282
+
283
+ /** The default test site plus a second tenant on its own host. */
284
+ function createDatabaseWithTwoSites() {
285
+ const testDb = createTestDatabase();
286
+ testDb.sqlite
287
+ .prepare(
288
+ `
289
+ INSERT INTO site (id, key, status, created_at, updated_at)
290
+ VALUES (?, 'other', 'active', 1774200001, 1774200001)
291
+ `,
292
+ )
293
+ .run(OTHER_SITE_ID);
294
+ testDb.sqlite
295
+ .prepare(
296
+ `
297
+ INSERT INTO site_domain (id, site_id, host, path_prefix, kind, redirect_to_primary, created_at, updated_at)
298
+ VALUES ('std_other_1', ?, 'other.localtest.me', NULL, 'primary', 1, 1774200001, 1774200001)
299
+ `,
300
+ )
301
+ .run(OTHER_SITE_ID);
302
+ return testDb;
303
+ }
304
+
305
+ // A hosted database holds every tenant, so there is no "only site" to fall
306
+ // back to. The single-site lookup would blame SITE_RESOLUTION_MODE, which is
307
+ // already host-based; the CLI has to say which flag picks the site.
308
+ it("requires a site selector in host-based mode with several sites", async () => {
309
+ const testDb = createDatabaseWithTwoSites();
310
+
311
+ await expect(
312
+ createNodeCliRuntime(
313
+ nodeDatabaseBindings(testDb, { SITE_RESOLUTION_MODE: "host-based" }),
314
+ ),
315
+ ).rejects.toThrow(
316
+ "host-based mode needs a target site. Pass --site <key|id>, --host <host>, or --url <url>.",
317
+ );
318
+ });
319
+
320
+ it.each<[string, CliSiteSelector]>([
321
+ ["key", { kind: "site", idOrKey: "other" }],
322
+ ["id", { kind: "site", idOrKey: OTHER_SITE_ID }],
323
+ ["host", { kind: "host", host: "other.localtest.me", pathPrefix: null }],
324
+ ])(
325
+ "scopes a host-based runtime to the site selected by %s",
326
+ async (_label, selector) => {
327
+ const testDb = createDatabaseWithTwoSites();
328
+
329
+ const runtime = await createNodeCliRuntime(
330
+ nodeDatabaseBindings(testDb, { SITE_RESOLUTION_MODE: "host-based" }),
331
+ selector,
332
+ );
333
+ await runtime.services.settings.set("PASSWORD_RESET_TOKEN", "hash:1");
334
+
335
+ expect(runtime.currentSite.id).toBe(OTHER_SITE_ID);
336
+ expect(runtime.currentSiteDomain?.host).toBe("other.localtest.me");
337
+ expect(
338
+ testDb.sqlite
339
+ .prepare(
340
+ `SELECT site_id FROM site_setting WHERE key = 'PASSWORD_RESET_TOKEN'`,
341
+ )
342
+ .pluck()
343
+ .all(),
344
+ ).toEqual([OTHER_SITE_ID]);
345
+ },
346
+ );
347
+
348
+ it("names the selector that matched no site", async () => {
349
+ const testDb = createDatabaseWithTwoSites();
350
+ const env = nodeDatabaseBindings(testDb, {
351
+ SITE_RESOLUTION_MODE: "host-based",
352
+ });
353
+
354
+ await expect(
355
+ createNodeCliRuntime(env, { kind: "site", idOrKey: "missing" }),
356
+ ).rejects.toThrow("No site found for --site missing.");
357
+ await expect(
358
+ createNodeCliRuntime(env, {
359
+ kind: "host",
360
+ host: "other.localtest.me",
361
+ pathPrefix: "/blog",
362
+ }),
363
+ ).rejects.toThrow(
364
+ 'No site found for host "other.localtest.me" and path prefix "/blog".',
365
+ );
366
+ });
367
+
368
+ it("uses the instance's one site in single-site mode", async () => {
369
+ const runtime = await createNodeCliRuntime(
370
+ nodeDatabaseBindings(createTestDatabase()),
371
+ );
372
+
373
+ expect(runtime.currentSite.id).toBe(DEFAULT_TEST_SITE_ID);
374
+ });
375
+
376
+ // Here the single-site error is the accurate one: the database really does
377
+ // belong to a host-based install.
378
+ it("reports a multi-site database in single-site mode as misconfigured", async () => {
379
+ await expect(
380
+ createNodeCliRuntime(nodeDatabaseBindings(createDatabaseWithTwoSites())),
381
+ ).rejects.toThrow("single-site mode found multiple sites in the database:");
382
+ });
383
+ });
@@ -24,6 +24,7 @@ describe("getInstanceReadiness", () => {
24
24
  }),
25
25
  ).resolves.toEqual({
26
26
  status: "ok",
27
+ version: "test-version",
27
28
  checks: {
28
29
  startupConfig: { ok: true },
29
30
  database: { ok: true },
@@ -40,6 +41,7 @@ describe("getInstanceReadiness", () => {
40
41
  }),
41
42
  ).resolves.toEqual({
42
43
  status: "error",
44
+ version: "test-version",
43
45
  checks: {
44
46
  startupConfig: {
45
47
  ok: false,
@@ -104,6 +106,26 @@ describe("getInstanceReadiness", () => {
104
106
  );
105
107
  });
106
108
 
109
+ // What a deploy check compares: the build that answers, and when its
110
+ // process started. A Worker has no process, so it reports only the build.
111
+ it("reports the build, and the process start where there is one", async () => {
112
+ const { sqlite } = createTestDatabase();
113
+ const env = {
114
+ AUTH_SECRET: "test-secret-with-enough-entropy-for-readiness",
115
+ NODE_SQLITE: sqlite,
116
+ };
117
+
118
+ const withoutStart = await getInstanceReadiness(env);
119
+ expect(withoutStart.version).toBe("test-version");
120
+ expect(withoutStart).not.toHaveProperty("startedAt");
121
+
122
+ const withStart = await getInstanceReadiness({
123
+ ...env,
124
+ NODE_STARTED_AT: 1789000000,
125
+ });
126
+ expect(withStart.startedAt).toBe(1789000000);
127
+ });
128
+
107
129
  it("reports a missing database binding", async () => {
108
130
  await expect(
109
131
  getInstanceReadiness({
@@ -111,6 +133,7 @@ describe("getInstanceReadiness", () => {
111
133
  }),
112
134
  ).resolves.toEqual({
113
135
  status: "error",
136
+ version: "test-version",
114
137
  checks: {
115
138
  startupConfig: { ok: true },
116
139
  database: {
@@ -52,6 +52,13 @@ export interface CloudflareRequestRuntime {
52
52
  export async function createCloudflareRequestRuntime(
53
53
  env: Bindings,
54
54
  publicRequestUrl: string,
55
+ options?: {
56
+ /**
57
+ * Anchor this request's first read to the primary instead of any replica.
58
+ * Set for requests that carry a session cookie — see `createRequestRuntime`.
59
+ */
60
+ anchorReadsToPrimary?: boolean;
61
+ },
55
62
  ): Promise<CloudflareRequestRuntime> {
56
63
  if (!env.DB) {
57
64
  throw new Error("Cloudflare runtime requires a DB binding.");
@@ -62,8 +69,19 @@ export async function createCloudflareRequestRuntime(
62
69
  throw new Error("AUTH_SECRET should be set after startup validation.");
63
70
  }
64
71
 
65
- // Use withSession() to enable D1 Read Replication.
66
- const session = env.DB.withSession();
72
+ // Use withSession() to enable D1 Read Replication. The constraint applies to
73
+ // the session's *first* query; every later query is anchored to the bookmark
74
+ // that one returns, so the whole request is sequentially consistent.
75
+ //
76
+ // Bookmarks are not carried across requests, so an unconstrained first read
77
+ // may land on a replica that has not caught up yet. For anonymous traffic
78
+ // that is fine and it is the point of replication. For a request holding a
79
+ // session cookie it is not: the session row is written on the primary, and a
80
+ // lagging replica answers "no such session" — the visitor is bounced to
81
+ // sign-in even though they never signed out.
82
+ const session = env.DB.withSession(
83
+ options?.anchorReadsToPrimary ? "first-primary" : "first-unconstrained",
84
+ );
67
85
 
68
86
  // Note: Drizzle ORM doesn't officially support D1DatabaseSession yet
69
87
  // (issue #2226), but it works at runtime.
@@ -89,6 +107,7 @@ export async function createCloudflareRequestRuntime(
89
107
 
90
108
  const servicesConfig = {
91
109
  databaseDialect: "sqlite" as const,
110
+ authSecret,
92
111
  bootstrapSite: getSingleSiteBootstrapOptions(env),
93
112
  enforceHostedMediaQuota: getSiteResolutionMode(env) === "host-based",
94
113
  hostedControlPlane: createHostedControlPlaneClient(env),
@@ -2,13 +2,39 @@ import { createCloudflareRequestRuntime } from "./cloudflare.js";
2
2
  import { createNodeRequestRuntime } from "./node.js";
3
3
  import type { Bindings } from "../types/bindings.js";
4
4
 
5
+ /**
6
+ * Whether a `Cookie` header carries a better-auth session token.
7
+ *
8
+ * Matched on the `.session_token` suffix so it holds for both the plain and
9
+ * the `__Secure-`-prefixed cookie name. This is a cheap "probably signed in"
10
+ * hint used to pick a database consistency mode — it does not verify the
11
+ * cookie's signature, and it must not be used to make an auth decision.
12
+ *
13
+ * @param cookieHeader - Raw `Cookie` request header, if any
14
+ * @returns `true` when a session token cookie appears to be present
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * hasSessionCookie("__Secure-better-auth.session_token=abc"); // true
19
+ * hasSessionCookie("theme=dark"); // false
20
+ * ```
21
+ */
22
+ export function hasSessionCookie(
23
+ cookieHeader: string | null | undefined,
24
+ ): boolean {
25
+ return !!cookieHeader?.includes(".session_token=");
26
+ }
27
+
5
28
  export async function createRequestRuntime(
6
29
  env: Bindings,
7
30
  publicRequestUrl: string,
31
+ request?: Request,
8
32
  ) {
9
33
  if (env.NODE_DATABASE || env.NODE_SQLITE) {
10
34
  return createNodeRequestRuntime(env, publicRequestUrl);
11
35
  }
12
36
 
13
- return createCloudflareRequestRuntime(env, publicRequestUrl);
37
+ return createCloudflareRequestRuntime(env, publicRequestUrl, {
38
+ anchorReadsToPrimary: hasSessionCookie(request?.headers.get("cookie")),
39
+ });
14
40
  }
@@ -12,14 +12,21 @@ import {
12
12
  shouldUseSecureCookies,
13
13
  } from "../lib/env.js";
14
14
  import { createHostedControlPlaneClient } from "../lib/hosted-control-plane.js";
15
+ import { ValidationError } from "../lib/errors.js";
15
16
  import { createMemoryRateLimiter } from "../lib/rate-limit-memory.js";
16
17
  import type { RateLimiter } from "../lib/rate-limit.js";
18
+ import { InstanceSetupSchema } from "../lib/schemas.js";
17
19
  import { createStorageDriver, type StorageDriver } from "../lib/storage.js";
18
20
  import {
19
21
  createHostedHandoffService,
20
22
  type HostedHandoffService,
21
23
  } from "../services/hosted-handoff.js";
24
+ import {
25
+ createBootstrapService,
26
+ type SetUpInstanceResult,
27
+ } from "../services/bootstrap.js";
22
28
  import { createServices, type Services } from "../services/index.js";
29
+ import { TRANSIENT_SINGLE_SITE_ID } from "../services/site.js";
23
30
  import type { Site, SiteDomain } from "../types/entities.js";
24
31
  import type { Bindings } from "../types/bindings.js";
25
32
  import {
@@ -27,6 +34,7 @@ import {
27
34
  getResolvedSiteBaseUrl,
28
35
  resolveCliSite,
29
36
  resolveRequestSite,
37
+ type CliSiteSelector,
30
38
  } from "./site.js";
31
39
 
32
40
  export interface NodeRequestRuntime {
@@ -144,6 +152,7 @@ export async function createNodeRequestRuntime(
144
152
 
145
153
  const servicesConfig = {
146
154
  databaseDialect,
155
+ authSecret,
147
156
  bootstrapSite: getSingleSiteBootstrapOptions(env),
148
157
  enforceHostedMediaQuota: getSiteResolutionMode(env) === "host-based",
149
158
  hostedControlPlane: createHostedControlPlaneClient(env),
@@ -175,9 +184,24 @@ export async function createNodeRequestRuntime(
175
184
  * Builds the runtime objects needed by local CLI commands.
176
185
  *
177
186
  * Unlike the request runtime, this path does not require auth configuration.
187
+ *
188
+ * @param env - Bindings with a resolved `NODE_DATABASE` or `NODE_SQLITE`
189
+ * @param selector - The site the command acts on. Required in host-based
190
+ * mode; single-site mode falls back to the instance's one site.
191
+ * @returns Services, storage and database scoped to that site
192
+ * @throws {Error} When host-based mode has no selector, or the selector
193
+ * matches no site
194
+ * @example
195
+ * ```ts
196
+ * const runtime = await createNodeCliRuntime(bindings, {
197
+ * kind: "site",
198
+ * idOrKey: "demo",
199
+ * });
200
+ * ```
178
201
  */
179
202
  export async function createNodeCliRuntime(
180
203
  env: Bindings,
204
+ selector: CliSiteSelector | null = null,
181
205
  ): Promise<NodeCliRuntime> {
182
206
  const nodeDatabase = env.NODE_DATABASE;
183
207
  const sqlite = env.NODE_SQLITE;
@@ -194,7 +218,7 @@ export async function createNodeCliRuntime(
194
218
 
195
219
  const slugIdLength =
196
220
  parseInt(getEnvString(env, "SLUG_ID_LENGTH") ?? "5", 10) || 5;
197
- const siteLookup = await resolveCliSite(db, env, databaseSchema);
221
+ const siteLookup = await resolveCliSite(db, env, selector, databaseSchema);
198
222
 
199
223
  return {
200
224
  currentSite: siteLookup.site,
@@ -212,3 +236,45 @@ export async function createNodeCliRuntime(
212
236
  storage: createStorageDriver(env),
213
237
  };
214
238
  }
239
+
240
+ /**
241
+ * Set up a self-hosted Node install without a browser, the way the two setup
242
+ * screens would. Backs `jant setup`.
243
+ *
244
+ * @param env - Bindings with a resolved `NODE_DATABASE`
245
+ * @param input - The owner's credentials and the site's answers, unvalidated
246
+ * @returns What setup found, and the site
247
+ * @throws {ValidationError} When the input fails `InstanceSetupSchema`
248
+ * @throws {Error} Whenever `BootstrapService.setUpInstance` refuses
249
+ * @example
250
+ * ```ts
251
+ * await setUpNodeInstance(bindings, {
252
+ * email: "owner@example.com",
253
+ * password: "correct horse battery",
254
+ * });
255
+ * ```
256
+ */
257
+ export async function setUpNodeInstance(
258
+ env: Bindings,
259
+ input: unknown,
260
+ ): Promise<SetUpInstanceResult> {
261
+ const nodeDatabase = env.NODE_DATABASE;
262
+ if (!nodeDatabase) {
263
+ throw new Error("Node setup requires a resolved database binding.");
264
+ }
265
+
266
+ const parsed = InstanceSetupSchema.safeParse(input);
267
+ if (!parsed.success) {
268
+ throw new ValidationError(
269
+ parsed.error.issues[0]?.message ?? "Invalid setup input.",
270
+ );
271
+ }
272
+
273
+ // No request resolved a site, and setup never reads the one it is bound to.
274
+ return createBootstrapService(nodeDatabase.db, TRANSIENT_SINGLE_SITE_ID, {
275
+ schema: nodeDatabase.schema,
276
+ databaseDialect: nodeDatabase.dialect,
277
+ bootstrapSite: getSingleSiteBootstrapOptions(env),
278
+ siteResolutionMode: getSiteResolutionMode(env),
279
+ }).setUpInstance(parsed.data);
280
+ }
@@ -5,6 +5,7 @@ import {
5
5
  getAuthSecretReadinessError,
6
6
  getHostBasedStartupConfigurationIssues,
7
7
  } from "../lib/startup-config.js";
8
+ import { CORE_VERSION } from "../lib/version.js";
8
9
  import { createSiteService } from "../services/site.js";
9
10
  import type { Bindings } from "../types/bindings.js";
10
11
 
@@ -15,6 +16,15 @@ export interface ReadinessCheckStatus {
15
16
 
16
17
  export interface InstanceReadinessResult {
17
18
  status: "ok" | "error";
19
+ /** The running build: the package version, then the build's commit id. */
20
+ version: string;
21
+ /**
22
+ * When the Node process started serving, in Unix seconds. With `version`,
23
+ * it lets a deploy check from outside that the instance answering is one
24
+ * started after the deploy, not an older one a rollback left running.
25
+ * Absent on Workers.
26
+ */
27
+ startedAt?: number;
18
28
  checks: {
19
29
  startupConfig: ReadinessCheckStatus;
20
30
  database: ReadinessCheckStatus;
@@ -117,6 +127,7 @@ export async function getInstanceReadiness(
117
127
  | "INTERNAL_ADMIN_TOKEN"
118
128
  | "NODE_DATABASE"
119
129
  | "NODE_SQLITE"
130
+ | "NODE_STARTED_AT"
120
131
  | "SITE_RESOLUTION_MODE"
121
132
  >,
122
133
  ): Promise<InstanceReadinessResult> {
@@ -125,6 +136,10 @@ export async function getInstanceReadiness(
125
136
 
126
137
  return {
127
138
  status: startupConfig.ok && database.ok ? "ok" : "error",
139
+ version: CORE_VERSION,
140
+ ...(env.NODE_STARTED_AT === undefined
141
+ ? {}
142
+ : { startedAt: env.NODE_STARTED_AT }),
128
143
  checks: {
129
144
  startupConfig,
130
145
  database,
@@ -17,7 +17,7 @@ import {
17
17
  type SiteLookupResult,
18
18
  TRANSIENT_SINGLE_SITE_ID,
19
19
  } from "../services/site.js";
20
- import { NotFoundError } from "../lib/errors.js";
20
+ import { NotFoundError, SiteUnavailableError } from "../lib/errors.js";
21
21
  import type { Bindings } from "../types/bindings.js";
22
22
 
23
23
  function logHostedSiteResolutionFailure(input: {
@@ -115,45 +115,116 @@ export async function resolveRequestSite(
115
115
  siteKey: resolved.site.key,
116
116
  siteStatus: resolved.site.status,
117
117
  });
118
- throw new NotFoundError("Site");
118
+ // The host resolved, so the site is real — it is just suspended (hosted
119
+ // plan ended, or an operator took it down). Serving a 404 here told the
120
+ // visitor the address was wrong and told crawlers to drop the site, both
121
+ // of which are false while it can still be restored.
122
+ throw new SiteUnavailableError();
119
123
  }
120
124
  return resolved;
121
125
  }
122
126
 
123
- export async function resolveCliSite(
127
+ /**
128
+ * The site a CLI command was told to act on. The `jant` CLI builds it from
129
+ * `--site <key|id>`, `--host <host>` with `--path-prefix`, or `--url <url>`
130
+ * (see `parseCliSiteSelector` in `bin/lib/site-selection.js`).
131
+ */
132
+ export type CliSiteSelector =
133
+ | { kind: "site"; idOrKey: string }
134
+ | {
135
+ kind: "host";
136
+ host: string;
137
+ /** The domain's stored prefix (`/blog`), or null for a root domain. */
138
+ pathPrefix: string | null;
139
+ };
140
+
141
+ /**
142
+ * Printed when a host-based CLI command names no site. Kept word for word with
143
+ * the raw-SQL resolver the D1 path uses in `bin/lib/site-selection.js`.
144
+ */
145
+ const HOST_BASED_CLI_SITE_REQUIRED_MESSAGE =
146
+ "host-based mode needs a target site. Pass --site <key|id>, --host <host>, or --url <url>.";
147
+
148
+ async function resolveSelectedCliSite(
124
149
  db: Database,
125
- env: Bindings,
126
- databaseSchema: DatabaseSchema = sqliteSchemaBundle,
150
+ selector: CliSiteSelector,
151
+ databaseSchema: DatabaseSchema,
127
152
  ): Promise<SiteLookupResult> {
128
153
  const siteService = createSiteService(db, databaseSchema);
129
- const resolutionMode = getSiteResolutionMode(env);
130
154
 
131
- if (resolutionMode === "single-site") {
132
- const resolved = await siteService.resolveSingleSite({
133
- ...getSingleSiteBootstrapOptions(env),
134
- createIfMissing: false,
135
- });
136
-
137
- if (resolved.site.id === TRANSIENT_SINGLE_SITE_ID) {
138
- throw new Error(
139
- "No site is configured for this instance yet. Finish /setup before running this command.",
140
- );
155
+ if (selector.kind === "site") {
156
+ const site = await siteService.getByIdOrKey(selector.idOrKey);
157
+ if (!site) {
158
+ throw new Error(`No site found for --site ${selector.idOrKey}.`);
141
159
  }
160
+ return {
161
+ site,
162
+ domain: await siteService.getPrimaryDomainForSite(site.id),
163
+ };
164
+ }
142
165
 
143
- return resolved;
166
+ const resolved = await siteService.resolveByHost(
167
+ selector.host,
168
+ selector.pathPrefix,
169
+ );
170
+ if (!resolved) {
171
+ throw new Error(
172
+ `No site found for host "${selector.host}"${selector.pathPrefix ? ` and path prefix "${selector.pathPrefix}"` : ""}.`,
173
+ );
144
174
  }
175
+ return resolved;
176
+ }
177
+
178
+ /**
179
+ * Resolve the site a CLI command acts on.
180
+ *
181
+ * A selector wins in either mode. Without one, single-site mode uses the
182
+ * instance's one site, and host-based mode fails before touching the
183
+ * database: a hosted database holds every tenant, so there is no default to
184
+ * fall back to — not even when it happens to hold one site today.
185
+ *
186
+ * @param db - The database
187
+ * @param env - Bindings that carry `SITE_RESOLUTION_MODE` and the single-site
188
+ * origin
189
+ * @param selector - The site the command was told to act on, if any
190
+ * @param databaseSchema - The schema bundle for the database's dialect
191
+ * @returns The site and its domain
192
+ * @throws {Error} When the selector matches no site, when host-based mode has
193
+ * no selector, or when single-site mode has not finished setup
194
+ * @example
195
+ * ```ts
196
+ * await resolveCliSite(db, env, { kind: "site", idOrKey: "demo" });
197
+ * ```
198
+ */
199
+ export async function resolveCliSite(
200
+ db: Database,
201
+ env: Bindings,
202
+ selector: CliSiteSelector | null = null,
203
+ databaseSchema: DatabaseSchema = sqliteSchemaBundle,
204
+ ): Promise<SiteLookupResult> {
205
+ if (selector) {
206
+ return resolveSelectedCliSite(db, selector, databaseSchema);
207
+ }
208
+
209
+ if (getSiteResolutionMode(env) === "host-based") {
210
+ throw new Error(HOST_BASED_CLI_SITE_REQUIRED_MESSAGE);
211
+ }
212
+
213
+ const resolved = await createSiteService(
214
+ db,
215
+ databaseSchema,
216
+ ).resolveSingleSite({
217
+ ...getSingleSiteBootstrapOptions(env),
218
+ createIfMissing: false,
219
+ });
145
220
 
146
- const onlySite = await siteService.getOnlySite();
147
- if (!onlySite) {
221
+ if (resolved.site.id === TRANSIENT_SINGLE_SITE_ID) {
148
222
  throw new Error(
149
- "CLI site selection for host-based mode is not implemented yet.",
223
+ "No site is configured for this instance yet. Finish /setup before running this command.",
150
224
  );
151
225
  }
152
226
 
153
- return {
154
- site: onlySite,
155
- domain: null,
156
- };
227
+ return resolved;
157
228
  }
158
229
 
159
230
  export function getResolvedSiteBaseUrl(